1. 什么是 $nextTick
$nextTick 是 Vue 提供的一个全局 API,允许延迟执行一个函数,直到下一次 DOM 更新循环结束之后。
简单来说,就是:等 Vue 把该更新的更新完了,再执行传入的这个函数。
2. 为什么需要 $nextTick
先来看一个例子:
{{ msg }}
<script setup>import { ref, nextTick, onMounted } from 'vue'const msg = ref('Hello')const box = ref(null)function updateText() {msg.value = 'Vue3 NextTick!'// 立即读 DOM 可能还是旧值或 null,不要在这里直接读 box.valueconsole.log('立即获取:', box.value && box.value.textContent)// 使用 nextTick 确保 DOM 更新完成后再读取nextTick(() => {console.log('nextTick 后获取:', box.value && box.value.textContent)})}onMounted(() => {updateText()})
</script>
要理解为什么需要 nextTick,就必须先理解 Vue 的 异步更新策略。
想象一下这个场景:
for (let i = 0; i < 100; i++) {this.counter++;
}
如果每执行一次 this.counter++,Vue 都立刻去更新 DOM,那么在瞬间内,DOM 就要被重新渲染 100 次。这会造成巨大的性能浪费。
为了避免这种情况,Vue 做了一个非常聪明的优化: 异步批量更新。
当你修改一个响应式数据时,Vue 并不会马上更新 DOM。相反,它会开启一个队列,把你这次修改以及在同一个“事件循环(tick)”中发生的所有其他数据修改都缓存起来。然后,在下一个“事件循环”中,Vue 会清空这个队列,只进行一次 DOM 更新,包含所有的数据变化。
这就产生了一个问题:
当你在代码中修改了数据后,如果想立即基于更新后的 DOM 状态去做一些操作(比如获取一个新渲染元素的高度),你是做不到的。因为此时,你的 JS 代码是同步执行的,而 Vue 的 DOM 更新是异步的。
这就是 $nextTick 登场的时刻。 它提供了一个桥梁,让你能够安全地“穿越”到 DOM 更新完成之后的那个时间点。
3. 使用方式
方式 1:回调方式
nextTick(() => {console.log('DOM 更新完毕!')
})
方式 2:await 方式
await nextTick()
console.log('DOM 更新完毕!')
4. 不同浏览器针对 $nextTick 的实现有区别吗?
是的,nextTick 的内部实现机制在不同浏览器(尤其是新旧浏览器)之间是有区别的。但 Vue 的高明之处在于,它通过一套优雅的“降级策略”(Fallback Mechanism),保证了 $nextTick 在所有支持的浏览器上,对外表现出一致的行为。
4.1. 核心目标:寻找最快的异步任务
Vue 实现 $nextTick 的核心目标只有一个:找到当前环境中最快的方式,将一个任务推迟到下一个 DOM 更新周期后执行。简单来说也就是等到 DOM 全部更新完再执行 nextTick 回调。
在浏览器事件循环(Event Loop)中,任务被分为两类:
- 宏任务(Macrotask):比如
setTimeout、setInterval、script(整体代码)、I/O 操作、UI rendering。 - 微任务(Microtask):比如
Promise.then、MutationObserver,queueMicrotask。
在一次事件循环中,所有微任务都会在下一个宏任务开始之前,以及下一次 UI 渲染之前全部执行完毕。
因此,微任务是实现 nextTick 的最理想方式,因为它能保证在浏览器重新渲染屏幕之前执行回调,时机最精确,速度最快。
4.2. Vue2 的实现:一套优雅的降级策略
在 Vue 源码中,会检测当前浏览器支持哪些 API,并按照 微任务 > 宏任务 的优先级顺序来选择一个“任务调度器”。
以下是 Vue2 中典型的降级链(优先级从高到低)
- 首选:Promise.then(微任务)
适用浏览器:所有现代浏览器(Chrome、Firefox、Safari、Edge 等),以及 IE11(需要 polyfill)。
工作原理:这是 ES6 的标准,它的 .then() 回调会被放入微任务队列。这是当前最通用、最标准的微任务实现方式。
状态:最佳选择。 - 备选 A:MutationObserver(微任务)
适用浏览器:主流浏览器都支持,包括 IE11。
工作原理:MutationObserver 用于监听 DOM 的变化。Vue 会创建一个观察者并监听一个文本节点的微小变化,当它执行 observer.observe() 后,会立即改变这个文本节点,从而触发回调。这个回调也是在微任务队列中执行的。
状态:优秀的备选方案。在没有原生 Promise 的环境(或 Promise 被不标准地 polyfill 时),是一个非常可靠的微任务实现。 - 备选 B:
setImmediate(宏任务-但有特殊性)
适用浏览器:仅限 IE10+和 Node.js。
工作原理:setImmediae设计初衷就是“立即”执行一个异步任务,在宏任务中,它的执行时机通常比setTimeout(fn,0)要早且更稳定。
状态:特定寒假下的宏任务最优解。因为它不是标准 API,所有使用范围很窄。 - 最终备选:setTimeout(fn,0)(宏任务)
适用浏览器:所有浏览器都支持。
工作原理:这是最后的兜底方案。setTimeout(fn,0) 会讲回调函数放入宏任务队列的末尾,等待下一个事件循环滴答(tick)时执行。
状态:兼容性最强但性能最差的方案,它的问题在于:
- 时机不精准:在两次宏任务之间,可能会发生 UI 渲染。这意味这回调可能在 DOM 更新并渲染后才执行。
- 最小延迟:即使设置为 0,浏览器实际上也有一个最小延迟时间(通常是 4ms 左右)。
4.3. Vue3 的变化
Vue 3 在 nextTick 的实现上做了简化。因为它放弃了对 IE11 等旧版浏览器的支持,所以它的实现可以更加现代和统一。
在 Vue 3 的浏览器构建版本中,nextTick 基本上只依赖 Promise.then 来调度微任务,不再需要 MutationObserver 或 setTimeout 等复杂的降级逻辑。这使得代码库更小、更高效。
4.4. 总结
实现方式 | 任务类型 | 优点 | 缺点 | 主要使用场景 |
Promise.then() | 微任务 | 标准、快速、时机精确 | 需要环境支持(现代浏览器) | Vue3 的唯一选择,Vue2 的首选 |
MutationObserver | 微任务 | 快速、时机精确、兼容性好 | 实现稍微复杂 | Vue2 在无原生 Promise 的备选 |
setImmediate | 宏任务 | 比 setTimeout 快 | 非标准仅 IE10+ | Vue2 在 IE 环境的备选 |
seTimeout(fn,0) | 宏任务 | 兼容所有浏览器 | 速度慢,时机不精确 | Vue2 的兜底方案 |
$nextTick 的底层实现在不同浏览器上是动态选择的。Vue 内部维护了一个优雅的降级策略,优先使用 Promise 等微任务,以保证回调能最快、最精确地在 DOM 更新后执行。在不支持微任务的旧浏览器中,它会降级使用 setTimeout 等宏任务作为兜底,从而在牺牲一点性能的情况下,保证了 API 行为的跨浏览器一致性。而到了 Vue 3,由于不再支持旧版浏览器,这个实现已经被大大简化,主要依赖 Promise。
5. 内部原理
通过上面的解析,内部实现原理就很好理解了。
$nextTick 的实现巧妙地利用了浏览器的 事件循环 (Event Loop) 机制。
它会把你的回调函数放进一个队列里,然后尝试以“微任务 (Microtask)”的方式,在当前同步代码执行完毕后、浏览器下次渲染之前,立即执行这个队列。
- 首选Promise.then:在现代浏览器中,Promise.then 的回调会被放入微任务队列。微任务会在当前宏任务(比如一次点击事件)执行结束后,但在下一次渲染开始前立即执行。这是最理想、最快的方式。
- 备选MutationObserver:如果环境不支持 Promise,Vue 会使用 MutationObserver,它也是一个微任务。
- 最后的备选 setTimeout(fn, 0):如果连微任务都不支持(极老的浏览器),Vue 会降级使用 setTimeout(fn, 0)。这会把回调函数放入宏任务队列 (Macrotask)。虽然比微任务慢,但也能确保在 DOM 更新之后执行。
5.1. 常见用例总结
- 访问更新后的 DOM:在修改数据后,获取元素的尺寸、内容或位置。
- 集成第三方库:当一个 DOM 元素通过 v-if 创建后,需要在这个元素上初始化一个第三方库(如图表、编辑器等)。
- 管理焦点:在一个输入框通过 v-if 显示后,立即让它获得焦点。