this.$nextTick你真的理解了吗?
Category(分类): Vue Status: 已校订(Vue 2 与 Vue 3)
nextTick 常被概括为“等 DOM 更新完再执行”,但这句话容易被扩大成三个错误结论:created + nextTick 一定能访问 ref、nextTick 会主动触发渲染、回调执行时浏览器一定已完成绘制。本文保留原文的源码学习路线,并给出更精确的版本边界。
一、nextTick 等待的是什么
Vue 会把同一轮同步代码中的多次响应式变更缓冲、去重,再批量更新组件 DOM。nextTick() 用于等待 Vue 当前待处理的 DOM update flush 完成。
Vue 3 的典型用法:
<script setup lang="ts">
import { nextTick, ref } from 'vue'
const count = ref(0)
const counter = ref<HTMLElement | null>(null)
async function increment() {
count.value++
// 此时 Vue 通常尚未 patch DOM
await nextTick()
// 此时可以读取本次 Vue 更新后的 DOM
console.log(counter.value?.textContent)
}
</script>
<template>
<button @click="increment">增加</button>
<p ref="counter">{{ count }}</p>
</template>
必须先发生状态变更,再 await nextTick()。如果没有待处理更新,nextTick 不会主动制造一次渲染。
二、created + nextTick 不是访问 ref 的生命周期保证
原文用下面这组完整实验形成了自己的结论。先保留实验,便于对照:
<template>
<div ref="test1">created:{{ message }}</div>
</template>
<script>
export default {
data() {
return {
message: 'Hello World!',
}
},
created() {
this.message = '你好,世界!'
// 为了在同一次复现实验中继续执行,用 try/catch 记录原来的直接读取错误
try {
console.log(this.$refs.test1.innerText)
} catch (error) {
console.error(error) // TypeError:test1 此时是 undefined
}
// 原实验在普通同步客户端挂载中成功打印“created:你好,世界!”
this.$nextTick(() => {
console.log('test1 nextTick:', this.$refs.test1.innerText)
})
},
}
</script>
直接读取报错的现象正确:created 时组件尚未挂载,模板 ref 尚不可用。同一钩子中的 $nextTick 回调在原作者特定的普通同步客户端挂载流程里,可能恰好排在首次挂载完成之后,所以该次实验能读取 DOM。
但这只是特定调度路径下的观察结果,不能形成“created + nextTick 一定能访问 ref”的生命周期保证。在尚未 mount、SSR、异步组件、Suspense 或 ref 位于未满足的 v-if 分支时,仍可能拿到 undefined/null。
首次访问模板 ref 的可靠写法是 mounted / onMounted:
// Options API
export default {
mounted() {
console.log(this.$refs.test1?.innerText)
},
}
<script setup lang="ts">
import { onMounted, useTemplateRef } from 'vue'
const test1 = useTemplateRef<HTMLElement>('test1')
onMounted(() => {
console.log(test1.value?.innerText)
})
</script>
<template>
<div ref="test1">已挂载</div>
</template>
结论应是:
- 首屏 DOM/ref 初始化:使用
mounted/onMounted; - 已挂载组件中,状态变化后读取新 DOM:在变化之后使用
nextTick; - ref 受
v-if、异步组件等控制时,还要等待相应条件真正成立。
三、Vue 2 的 nextTick
Vue 2 会把回调加入 callbacks 队列,并用一个 pending 标记避免重复安排 flush。Vue 2.7 源码选择异步机制的大致顺序是:
- 原生
Promise.then; - 非 IE 且满足检测条件的
MutationObserver; - 原生
setImmediate; setTimeout(flushCallbacks, 0)。
截图版本提示:
05-01标注的是 Vue 2.6.14 历史源码截图,不是下文文字基准 Vue 2.7.16;调度主体相近,细节应以相应版本源码为准。


3.1 Promise 分支
优先项要求通过源码的原生能力检测,不是任意 Promise polyfill。Vue 2 自身不提供 Promise polyfill;早期 iOS UIWebView 还包含额外空 setTimeout 的兼容 workaround。

3.2 MutationObserver 分支
MutationObserver 用于异步观察 DOM 变更。Vue 2 的 fallback 会创建文本节点并切换其内容,以安排回调刷新;目的不是“操作 DOM 来保证浏览器已经绘制”,而是利用 MutationObserver 的异步通知机制。
childList、attributes、characterData、subtree、attributeOldValue、characterDataOldValue、attributeFilter 是调用 observe(target, options) 时的观察选项字段,不是 MutationObserver 实例的“七个属性”。
截图纠错:
05-04同属 Vue 2.6.14 历史截图,其中“DOM 任意变化即可保证拿到最新 DOM”等原注释不严谨。MutationObserver 分支只是安排异步 flush,不保证任意 DOM、Vue patch 或浏览器 paint 均已完成;正文以对应版本源码边界为准。

3.3 setImmediate / setTimeout 分支
这两项是前面机制不可用时的宏任务 fallback。setImmediate 不是“立刻同步执行”,setTimeout(fn, 0) 也只表示最小延迟请求,并不保证零毫秒执行。

3.4 Vue 2 用法
this.$nextTick(() => {
// this 绑定为当前组件实例
})
await this.$nextTick()
this.$nextTick().then(() => {
// 注意必须先调用 $nextTick()
})
Vue 2 全局 API Vue.nextTick([callback, context]) 有历史上的 context 参数;实例 $nextTick 会绑定当前实例。无 callback 时,仅在环境支持 Promise 的前提下返回 Promise。因此不能无条件说“Vue 2 nextTick 就是 Promise”。
四、Vue 3 的 nextTick 与 currentFlushPromise
Vue 3 当前实现的核心非常简洁:
export function nextTick<T = void, R = void>(
this: T,
fn?: (this: T) => R,
): Promise<Awaited<R>> {
const p = currentFlushPromise || resolvedPromise
return fn ? p.then(this ? fn.bind(this) : fn) : p
}
理解重点不是“它封装成了 Promise”,而是 currentFlushPromise:
- 有正在等待的 scheduler flush 时,
nextTick链到该 Promise,等待主 job 队列和 post-flush callbacks 等本轮调度工作; - 没有待处理 flush 时,使用已 resolved 的 Promise,只把后续代码放到一个 microtask,不会主动更新 DOM;
- 多次同步状态变更通常由同一个 flush 批处理。
Vue 3 全局 nextTick(callback?) 没有 Vue 2 的公开 context 参数;Options API 的 this.$nextTick 是实例绑定版本。
import { nextTick } from 'vue'
state.message = '你好,世界!'
await nextTick()
// 读取 Vue 本次 patch 后的 DOM
五、nextTick 不等于浏览器 paint
await nextTick() 通常意味着 Vue 已完成当前批次的 DOM patch,但不保证:
- 浏览器已经把新画面绘制到屏幕;
- CSS 布局、过渡或动画已经完成;
- 图片、字体、网络请求已经加载;
- 异步组件或未成立的
v-if已出现; - 第三方库自己的异步任务已完成。
requestAnimationFrame 回调是在下一次绘制之前执行,适合在浏览器准备下一帧时读取布局或安排视觉更新;单次 rAF 不表示该帧已经 paint 到屏幕。若只需要进入下一帧的绘制前回调,可这样写:
await nextTick()
await new Promise((resolve) => requestAnimationFrame(resolve))
// 已进入下一帧绘制前回调;不能据此宣称该帧已 paint
若目标是让一次绘制有机会发生后再继续,实践中常见双 rAF:
await nextTick()
await new Promise((resolve) => {
requestAnimationFrame(() => requestAnimationFrame(resolve))
})
双 rAF 也不是所有环境和业务目标的绝对保证,后台标签页还可能被节流,仍需按场景验证。等待 CSS 过渡应监听 transitionend 或使用 Vue Transition 钩子;等待图片、字体或第三方库则要等待各自的完成信号。
六、事件循环示例
下面的输出顺序仍适合帮助理解微任务与宏任务:
console.log('同步代码 1')
setTimeout(() => {
console.log('setTimeout')
}, 0)
Promise.resolve().then(() => {
console.log('promise.then')
})
console.log('同步代码 2')
输出:
同步代码 1
同步代码 2
promise.then
setTimeout
不过“nextTick 属于微任务”只适合作为 Vue 3 或 Vue 2 命中 Promise/MutationObserver 分支时的简化理解。Vue 2 fallback 到 setImmediate/setTimeout 时是宏任务。真正重要的是 Vue 的调度边界,而不是背一句固定的事件循环口诀。
总结
nextTick等待 Vue 当前批次的 DOM update flush,不主动触发更新。created时不能直接访问模板 ref;首次 DOM/ref 初始化应使用mounted/onMounted,不能把created + nextTick当作保证。- Vue 3
nextTick基于currentFlushPromise || resolvedPromise;没有待处理更新时只是等待后续 microtask。 nextTick不保证浏览器已 paint,也不等待网络、动画或异步组件。- Vue 2 fallback 顺序是原生 Promise → MutationObserver → 原生 setImmediate → setTimeout,并有环境检测与兼容细节。
- Vue 2 和 Vue 3 API 签名不同;
this.$nextTick().then(...)中的括号不能省略。
官方参考
- Vue 3:nextTick
- Vue 3:组件实例
$nextTick - Vue 3:模板 ref
- Vue 3:onMounted
- Vue 3 scheduler 源码
- Vue 2:异步更新队列
- Vue 2:Vue.nextTick API
- Vue 2.7:next-tick 源码
作者:路灯下的光 原文链接:https://juejin.cn/post/7177681326861418556 来源:稀土掘金。著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。