03.nextTick 实现原理
约 701 字大约 2 分钟
2026-03-01
面试题目
nextTick 的本质是将回调函数包装为微任务放入微任务队列,浏览器完成渲染后会优先执行微任务
| 版本 | 实现策略 |
|---|---|
| Vue2 | 为兼容旧浏览器,按环境选择:Promise → MutationObserver → setImmediate(IE) → setTimeout 兜底 |
| Vue3 | 仅考虑现代浏览器,直接用 Promise.resolve() 包装,代码更简洁,性能更高 |
在这个场景中,点击按钮页面只会渲染一次而不是 1000 次,因为 Vue 的响应式系统会合并多次修改,并异步地更新 DOM
而这么做的原因也很简单,因为频繁地更新 DOM 会导致频繁的重绘和重排,非常耗费性能。但是异步更新会带来一个新问题,就是无法及时获取到更新后的 DOM 值。因为获取 DOM 数据是同步代码,而 DOM 的更新是异步的,同步代码会先于异步代码执行
代码
<template>
<div>
<p>{{ count }}</p>
<button @click="increment">增加计数</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
const increment = () => {
for (let i = 1; i <= 1000; i++) {
count.value = i
}
}
</script>核心原理
将回调包装成微任务,放入微任务队列,浏览器完成渲染后优先执行微任务
手动实现
const increment = () => {
count.value++
Promise.resolve().then(() => {
console.log('最新的数据:', count.value)
console.log('通过DOM拿textContent数据:', counterRef.value.textContent)
console.log('通过DOM拿innerHTML数据:', counterRef.value.innerHTML)
})
}而事实也的确如此,在 Vue 源码中 nextTick 就是基于 Promise 实现的
源码
const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise<any>
let currentFlushPromise: Promise<void> | null = null
function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}
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
}| 变量/逻辑 | 说明 |
|---|---|
resolvedPromise | Promise.resolve(),用于创建微任务 |
currentFlushPromise | 当前刷新任务对应的 Promise,DOM 更新由 flushJobs 完成 |
queueFlush | 将 flushJobs 放入微任务队列 |
nextTick | 若有 currentFlushPromise 则在其后执行回调,否则在 resolvedPromise 后执行,保证 DOM 更新完成后再跑回调 |
题目
如下代码分别渲染几次?为什么?
<template>
<div>{{ count }}</div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
for (let i = 0; i < 5, i++) {
setTimeout(() => {
count.value = i
}, 0)
}
</setup>解析
一共渲染 6 次,原因如下:
- 初始化渲染一次
- 每个
setTimeout回调在各自的宏任务里执行,每个宏任务里:执行一次count.value = i,Vue 会在当前宏任务结束后、微任务阶段做一次 flush 操作,触发渲染,因此会渲染 5 次
与不加 setTimeout 的代码相比差别在于:不加 setTimeout 时:5 次 count.value = i 在同一轮同步执行里完成 → Vue 只调度 1 个微任务 flush → 合并成 1 次 DOM 更新
