nextTick 实现原理
一句话概括
nextTick 是 Vue 的异步更新”守门员”——它将同一事件循环内的多次数据变更合并为一次 DOM 更新,回调在 DOM 更新完成后执行。底层利用微任务(Promise.then)实现。
核心知识点
1. nextTick 解决的问题
1
2
3
4
5
6
7
8
9
10
11
const count = ref(0);
count.value = 1;
console.log(document.querySelector('#app').textContent); // 还是旧的 "0"!
nextTick(() => {
console.log(document.querySelector('#app').textContent); // "1" ✅
});
count.value = 2; // 同一 tick 的多次修改
count.value = 3; // Vue 只会更新一次 DOM,最终显示 3
原因:Vue 不会在每次数据变更后立即更新 DOM,而是将更新任务推入微任务队列,在本轮事件循环末尾统一处理。
2. 任务队列机制——promise.then
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 简化版 nextTick 实现
const queue = [];
let isFlushPending = false;
function nextTick(fn) {
return fn
? Promise.resolve().then(fn) // 注册回调
: Promise.resolve(); // 无回调时返回一个 resolved Promise
}
function queueJob(job) {
if (!queue.includes(job)) queue.push(job); // 去重
if (!isFlushPending) {
isFlushPending = true;
Promise.resolve().then(flushJobs); // 仅注册一次微任务
}
}
function flushJobs() {
isFlushPending = false;
queue.forEach(job => job());
queue.length = 0;
}
关键设计:isFlushPending 确保多个数据变更只注册一个微任务,queue.includes(job) 去重防止同一个组件渲染多次。
3. pre / post 队列
1
2
3
4
5
6
7
8
// Vue 3 实际有两个队列
const pendingPreFlushCbs = []; // pre 队列(组件更新前)
const pendingPostFlushCbs = []; // post 队列(DOM 更新后)
// 执行顺序:
// 1. 清空 pre 队列(computed 更新、watch:flush=pre)
// 2. 执行组件 render
// 3. 清空 post 队列(watch:flush=post、nextTick 回调)
4. nextTick 的两种用法
1
2
3
4
5
6
7
8
9
10
11
12
import { ref, nextTick } from 'vue';
// 用法 1:回调函数
const count = ref(0);
count.value++;
nextTick(() => {
console.log('DOM 已更新');
});
// 用法 2:await
await nextTick();
console.log('DOM 已更新');
5. 为什么是微任务不是 setTimeout
1
2
3
4
5
// ❌ 宏任务(setTimeout):每次数据变更后等一轮事件循环才更新 DOM
// 用户可能看到中间态:先闪一下旧值,再闪新值
// ✅ 微任务(Promise.then):当前 tick 末尾执行
// 用户在浏览器渲染前看不到中间态
其实你每天都在用
- v-for 渲染后操作 DOM:
data.push(item); await nextTick(); scrollToBottom()——先等列表渲染完再滚到底部 - 表单验证后聚焦输入框:
error.value = '必填'; await nextTick(); inputRef.value.focus() - 组件 ref 的使用:
onMounted里拿ref保证 DOM 就位——其实 Vue 内部也是靠 nextTick 调度的 - 测试框架(Vue Test Utils):
await wrapper.vm.$nextTick()等 DOM 更新完再断言 - echarts 初始化:
await nextTick(); echarts.init(containerRef.value)——必须先有 DOM 节点
常见误解
❌ 误区:「
await nextTick()只在 Vue 组件内有效」nextTick是全局 API,任何 JS 环境都能用(只要有 Promise)。它不依赖组件实例,模块外也能import { nextTick } from 'vue'。❌ 误区:「多次调用
nextTick会多次更新 DOM」 不会。count.value = 1; count.value = 2; count.value = 3在同一 tick 合并为一次更新。即便中间穿插await nextTick(),也只是等这一轮更新完再继续。❌ 误区:「nextTick 就是
setTimeout(fn, 0)」 Vue 内部会按环境选择最优方案:Promise.then→MutationObserver→setImmediate→setTimeout。绝大多数现代浏览器走 Promise 微任务路径,比 setTimeout(宏任务)早一个阶段执行。❌ 误区:「在 watch 的回调里不需要 nextTick」 看 flush。
flush: 'pre'(默认)的回调在 DOM 更新前执行,此时拿 DOM 还是旧的;flush: 'post'在 DOM 更新后执行。如果你用默认 watch 回调操作 DOM,必须里面再套await nextTick()。
一句话总结
nextTick = 数据变了先记账 → 微任务结算 → DOM 更新 → 你的回调执行——把多次操作合并成交,实现”感知不到延迟”的异步批量更新。