你真的理解 $nextTick 么?
原文从浏览器 Event Loop、微任务/宏任务和 Vue 2 源码分析
nextTick。这些内容仍然适合学习,但原文把浏览器线程、渲染时机和任务队列说得过于绝对,并混入了 Vue 2.5/2.6 的抓取代码。本文保留主线,补充 Vue 2.7、Vue 3.5 的用法和边界。
一、为什么叫 nextTick
Vue 的 nextTick 不是 Node.js 的 process.nextTick,只是名字相似。
它解决的问题是:响应式状态改变后,Vue 通常不会同步把每一次改变都立即写入 DOM,而是把组件更新任务放入调度队列,在当前同步代码结束后批量刷新。调用 nextTick 可以等待这次 Vue DOM 更新 flush 完成:
import { nextTick, ref } from 'vue'
const count = ref(0)
function update() {
count.value++
// 这里读取 DOM,通常还可能是旧内容
return nextTick()
}
async function handleClick() {
await update()
// 这里可以读取 Vue 本轮更新后的 DOM
}
Vue 2 使用实例方法:
this.message = 'new value'
this.$nextTick(() => {
// Vue 2 本轮 DOM 更新完成
})
Vue 3 推荐从 vue 导入:
message.value = 'new value'
await nextTick()
Vue 3 Options API 仍然可以使用 this.$nextTick();Composition API 通常使用 nextTick()。
二、浏览器事件循环的基础
1. 不要把浏览器线程图当成规范
浏览器是多进程、多线程系统,但“JS 引擎线程、GUI 渲染线程、事件触发线程完全分离,二者互斥”的简化说法容易误导。
对前端代码更有用的模型是:
- JavaScript 回调通常在页面的主线程上执行;
- 浏览器从不同 task source 取出任务执行;
- 一段任务执行期间,JavaScript 代码是串行的;
- 当前任务结束后会进行 microtask checkpoint;
- 浏览器是否在此后提供渲染机会由用户代理决定,不能保证每个 task 之后都立即绘制。

2. Task 与 microtask
常见的浏览器任务包括:
- 初始脚本执行;
- click、键盘、定时器、网络等事件回调;
setTimeout、setInterval回调。
常见 microtask 包括:
Promise.then/catch/finally;queueMicrotask();MutationObserver回调。
一个简化顺序是:
取出一个 task
↓
执行同步 JavaScript
↓
清空当前 microtask 队列
↓
浏览器可能进行样式计算、布局和绘制
↓
取出下一个 task
microtask 通常会在当前 task 结束后、下一次渲染机会前执行,但浏览器渲染调度存在实现和负载差异。不要把“宏任务 > 渲染 > 宏任务”当成每一轮都严格成立的规范。
process.nextTick 是 Node.js 特有的高优先级队列,不是浏览器 microtask 的标准 API;setImmediate 也不是浏览器标准 API。Object.observe 已废弃,不能列为现代浏览器的 microtask 方案。

三、用例:同步代码、Promise、MutationObserver 和定时器
下面的代码可以帮助观察队列顺序:
<div class="outer">
<div class="inner"></div>
</div>
<script>
const inner = document.querySelector('.inner')
const outer = document.querySelector('.outer')
new MutationObserver(records => {
console.log('mutation', records.length)
}).observe(outer, { attributes: true })
function onClick() {
console.log('click')
setTimeout(() => {
console.log('timeout')
}, 0)
Promise.resolve().then(() => {
console.log('promise')
})
outer.setAttribute('data-random', Math.random())
}
inner.addEventListener('click', onClick)
</script>
一般可以观察到 click 先打印,随后 Promise microtask 和 MutationObserver 回调在当前 task 结束时执行,setTimeout 留到后续 task。具体日志顺序和 MutationObserver 的合并方式应以当前浏览器实测为准,不应把一张任务队列对象图当作规范。
如果同时给 outer 添加 click 监听,事件冒泡会让两个监听器在同一个 click task 中同步执行,不会因此产生两个独立的 click task:
outer.addEventListener('click', onClick)
两个监听器产生的 Promise 会进入同一个 microtask 队列;MutationObserver 可能把同一轮多个 mutation records 合并到一次回调中。
四、Node.js 的 process.nextTick
下面代码中的 process.nextTick 只适用于 Node.js:
setTimeout(() => {
console.log('timeout')
}, 0)
process.nextTick(() => {
console.log('nextTick 1')
})
Promise.resolve().then(() => {
console.log('promise')
})
process.nextTick(() => {
console.log('nextTick 2')
})
在常见 Node.js 版本中,process.nextTick 队列会在当前操作结束后、Promise microtask 之前处理;Node 版本和具体阶段可能影响完整顺序。它与 Vue 的 nextTick 没有 API 继承关系,也不能用 Node 的执行顺序推导浏览器或 Vue 的 DOM 更新顺序。
五、Vue 为什么要异步批量更新
import { nextTick, ref } from 'vue'
const count = ref(0)
function updateManyTimes() {
count.value++
count.value++
count.value++
}
同一个同步任务中连续修改状态时,Vue 通常会去重组件更新 job,只在这一轮刷新一次渲染结果。这样可以避免每个赋值都立即执行完整的组件树更新。
<script setup lang="ts">
import { nextTick, ref } from 'vue'
const count = ref(0)
const element = ref<HTMLElement | null>(null)
async function increment() {
count.value++
console.log(element.value?.textContent) // 可能仍是旧值
await nextTick()
console.log(element.value?.textContent) // 本轮 Vue 更新后的值
}
</script>
<template>
<p ref="element">{{ count }}</p>
<button @click="increment">增加</button>
</template>
nextTick 等待的是 Vue 的更新 flush,不是“等待一个固定的 0ms 定时器”,也不是“保证浏览器已经完成绘制”。如果需求是等待浏览器渲染机会,可以在 DOM 更新后再使用 requestAnimationFrame:
await nextTick()
await new Promise<void>(resolve => {
requestAnimationFrame(() => resolve())
})
// 更接近下一次渲染机会,但仍不应把它当作跨浏览器的绘制完成承诺
六、Vue 2.7 的 nextTick 实现思路
Vue 2.7 的源码延续了 callbacks 队列和 timer fallback 的设计。下面是去掉 Flow、错误拼接和兼容分支后的教学版:
const callbacks = []
let pending = false
let timerFunc
function flushCallbacks() {
pending = false
const copies = callbacks.slice()
callbacks.length = 0
for (const callback of copies) {
callback()
}
}
if (typeof Promise !== 'undefined') {
const resolved = Promise.resolve()
timerFunc = () => resolved.then(flushCallbacks)
} else if (typeof MutationObserver !== 'undefined') {
const textNode = document.createTextNode('0')
let counter = 0
new MutationObserver(flushCallbacks).observe(textNode, {
characterData: true,
})
timerFunc = () => {
textNode.data = String(counter ^= 1)
}
} else if (typeof setImmediate !== 'undefined') {
timerFunc = () => setImmediate(flushCallbacks)
} else {
timerFunc = () => setTimeout(flushCallbacks, 0)
}
function nextTick(callback, context) {
let resolvePromise
callbacks.push(() => {
if (callback) {
callback.call(context)
} else if (resolvePromise) {
resolvePromise(context)
}
})
if (!pending) {
pending = true
timerFunc()
}
if (!callback && typeof Promise !== 'undefined') {
return new Promise(resolve => {
resolvePromise = resolve
})
}
}
这段代码是解释思路,不是建议复制进项目的 polyfill。Vue 2.5 在某些场景组合使用过 macro task 和 microtask;Vue 2.6/2.7 又主要回到 microtask,并保留兼容回退。不同 minor 版本的源码不能混写成一个“Vue nextTick 源码”。
Vue 2 $nextTick 的关键点是:
- 把回调放入 callbacks 队列;
- 同一轮只安排一次 flush;
- flush 时复制并清空 callbacks;
- 没有回调且环境支持 Promise 时返回 Promise;
- 更新 watcher 队列和 nextTick 队列配合完成 DOM 更新后的回调。
七、Vue 3.5 的 scheduler 与 nextTick
Vue 3 使用 scheduler 管理组件更新 job、前置 watcher、后置 watcher 和当前 flush Promise。实现细节会随版本变化,但可以用下面的模型理解:
const resolvedPromise = Promise.resolve()
let currentFlushPromise: Promise<void> | null = null
function nextTick(fn?: () => void) {
const promise = currentFlushPromise || resolvedPromise
return fn ? promise.then(fn) : promise
}
真实 runtime 还包含 job 去重、递归更新保护、pre/post flush callbacks 和异常处理。nextTick 的 Promise 会关联当前待处理的 Vue flush;如果当前没有待处理更新,它可能只是基于已 resolved 的 Promise 排队一个 microtask。
Vue 3 可以通过 watcher 的 flush 选择时机:
watch(source, callback, { flush: 'pre' }) // 默认语义附近
watch(source, callback, { flush: 'post' }) // 组件 DOM 更新后
watchSyncEffect(callback) // 谨慎:同步执行
如果只是等待一次响应式 DOM 更新,优先使用 await nextTick();如果是长期监听 DOM 更新后的副作用,可考虑 flush: 'post'。

八、常见误区
误区 1:nextTick 等于 setTimeout(fn, 0)
不等于。现代 Vue 和现代浏览器通常优先使用 Promise microtask;Vue 2 会根据环境选择 Promise、MutationObserver、setImmediate 或 setTimeout 回退。
误区 2:调用 nextTick 就保证已经绘制到屏幕
不保证。它保证 Vue 本轮 DOM 更新 flush 完成,浏览器绘制时机仍由渲染调度决定。需要动画帧配合时使用 requestAnimationFrame。
误区 3:每次赋值都必须 nextTick
不需要。只有在同一段代码中要读取更新后的 DOM、测量尺寸或调用依赖最新 DOM 的第三方库时才需要等待。
误区 4:Vue 的 nextTick 和 Node 的 process.nextTick 是一回事
不是。前者是 Vue 等待自身 DOM 更新的 API,后者是 Node.js 事件循环中的专用调度 API。
误区 5:microtask 一定比 task 更好
不一定。microtask 过多可能长时间占用主线程、推迟渲染;Vue 2 历史版本在事件冒泡和过渡时机上就曾在 microtask 与 macro task 之间权衡。应以用户交互和实际性能为依据。
九、面试版回答
Vue 的响应式更新通常会被放入异步调度队列,并在当前同步代码结束后批量刷新。
nextTick用来等待 Vue 下一轮 DOM 更新完成,所以修改响应式数据后,如果要读取更新后的 DOM,可以使用 Vue 2 的this.$nextTick()或 Vue 3 的await nextTick()。它等待的是 Vue 的 flush,不等同于 Node 的process.nextTick、setTimeout(0)或浏览器已经绘制完成。Vue 2.7 使用 callbacks 队列和环境回退,Vue 3.5 使用 scheduler 与 Promise flush。
参考资料
- Vue 2
$nextTick - Vue 2 异步更新队列
- Vue 3
nextTick - Vue 3 生命周期与 DOM 更新
- Vue 3.5 scheduler 源码
- HTML Standard:事件循环
- Tasks, microtasks, queues and schedules
原文作者:子弈。原文的 Event Loop、Vue 2.5/2.6 源码分析和示例主线予以保留,推广内容、抓取噪声及过度绝对化的线程/渲染表述已删除或修正。