两次 nextTick ≠ 一次 setTimeout
nextTick 是事件循环的"VIP插队通道",而 setTimeout 是"普通候车区"——前者追求极致即时性,后者保证公平排队。99%的场景下,一次await就够了,滥用 nextTick 是性能灾难。
两次 nextTick 和 一次 setTimeout 不一样,而且差异非常关键。
核心区别概览(一句话版)
| 名称 | 触发时机 | 属于 | 会不会让 DOM 真刷新? | 常见用途 |
|---|---|---|---|---|
| nextTick | 本轮事件循环结束、DOM patch 完成之后 | 微任务 / Vue 自己的队列 | 会 | 等 DOM 更新完成后立刻拿到最新 DOM |
| 连续两次 nextTick | 仍然只会等到当前 DOM 更新完成(Vue 会合并) | 微任务 | 不会强制两次刷新 | 多数情况下与 1 次一样 |
| setTimeout(fn, 0) | 下一轮宏任务 | 宏任务 | 浏览器可能会进行一次完整渲染 | 等浏览器真正渲染后执行(晚于 nextTick) |
所以:两次 nextTick 不会等同 setTimeout
但有些情况下,开发者会用 setTimeout 作为“强制延后到下一帧”的方式,这点 nextTick 做不到。
为什么两次 nextTick 不等于 setTimeout?
1. 一次 nextTick
假设如下更新代码
visible.value = true
await nextTick()
示意图
同步代码:visible = true
│
▼
Vue 执行 DOM Patch(把元素插入 DOM)
│
▼
────────── 微任务队列(nextTick)──────────
nextTick 回调被执行(DOM 已更新,但浏览器尚未渲染)
────────────────────────────────────────
│
▼
浏览器 Layout & Paint(渲染到屏幕)
nextTick 能保证 DOM 已插入,不能保证浏览器已经渲染(layout & paint)。
2. 两次 nextTick
await nextTick()
await nextTick()
并不等同:nextTick → 渲染 → nextTick
而是:
同步代码
│
▼
Vue DOM Patch
│
▼
────────── 微任务队列(1) ──────────
nextTick 回调 1 执行
────────── 微任务队列(2) ──────────
nextTick 回调 2 执行(仍在渲染前)
────────────────────────────────────
│
▼
浏览器 Layout & Paint
两次 nextTick 仍然在“微任务楼层”,不能代替 setTimeout。
3. setTimeout
同步代码
│
▼
Vue DOM Patch
│
▼
────────── 微任务队列(nextTick)──────────
(nextTick 回调全部执行完)
─────────────────────────────────────────
│
▼
浏览器渲染(Layout → Paint)
│
▼
────────── 宏任务队列(setTimeout)───────
setTimeout(fn) 被执行(一定晚于渲染)
─────────────────────────────────────────
4. 结论
1、两次 nextTick 不会让 DOM 多渲染一次
Vue 的 DOM 更新是批处理的,无论写多少次,它们最终 都只会等待同一轮 DOM patch 结束。Vue 会把多个 nextTick 任务放到同一个队列里。
所以最终效果与一次 await nextTick() 几乎相同。
补充:具体区别写在最后。
2、setTimeout 会强制进入下一轮宏任务
这意味着:浏览器会在这期间有机会执行布局、绘制,所以 setTimeout 通常能获得“真正渲染之后的 DOM 状态” 是一种更“保险”的延迟方式。
场景对比(在 Vue 项目中的实际情况)
1、更新数据后立即操作最新 DOM
例如:
<div v-if="show" ref="box"></div>
show.value = true
await nextTick()
console.log(box.value) // 正确:此时 DOM 已经插入
这里用 nextTick 是正确的,用 setTimeout 会更慢,不需要。
2、DOM 更新完成,但浏览器还没真正渲染(如动画、获取尺寸不准)
例如在做动画、获取 offsetHeight:
show.value = true
await nextTick()
const height = box.value.offsetHeight
有时候会发现尺寸不准:浏览器还未真正布局(layout)。此时会写:
show.value = true
await nextTick()
setTimeout(() => {
console.log(box.value.offsetHeight) // 这次一定准
})
为什么 nextTick 不够?
因为 nextTick 只保证 “DOM patch 完成”,但不会等待浏览器 layout → paint。setTimeout 才能确保浏览器有机会进行渲染。
3、强制等待下一帧(如动画开始、CSS 过渡触发)
例如:
isOpen.value = false
await nextTick()
isOpen.value = true // 过渡无效(合并了)
可能写:
await nextTick()
setTimeout(() => {
isOpen.value = true // 过渡触发成功
}, 0)
或者更标准:
requestAnimationFrame(() => {
isOpen.value = true
})
两次 nextTick 也没用,因为仍然在同一轮 DOM patch 里。
4、在自定义组件中等待内部渲染
例如使用 vant/element-plus/自定义组件,有时组件内部还有一层 nextTick。
通常会写:
await nextTick()
await nextTick() // 等子组件内部 nextTick
在这种情况下,“两次 nextTick” 确实有意义。
但这个场景本质是:不在等浏览器渲染,而是在等子组件内部的 nextTick 队列。仍然 ≠ setTimeout。
这个场景尤其在切换表单元素的必填校验中十分常见。
总结一句话
两次 nextTick ≠ setTimeout
-
nextTick:等 Vue DOM 更新 → 微任务,早
-
setTimeout:等浏览器渲染 → 宏任务,晚
正确使用方式
| 想要的效果 | 用什么 |
|---|---|
| 等 DOM 更新完就操作 | nextTick |
| 等真正渲染完成(尺寸准确、动画需触发) |
setTimeout / requestAnimationFrame |
| 等子组件里的 nextTick | 连续两次 nextTick / setTimeout |
| 强制下一帧 |
requestAnimationFrame |
补充 await nextTick() 连续调用 vs 单次调用
核心区别:微任务拆分粒度和执行中断点不同。三次 await nextTick() 会创建三个独立的微任务,而单次调用只创建一个微任务。
1、执行机制对比
假设这是 Promise 封装的版本(常见于 Vue 或手动封装):
const nextTick = () => new Promise(resolve => process.nextTick(resolve));
// 方案A:三次await
async function methodA() {
console.log('A1');
await nextTick();
console.log('B1');
await nextTick();
console.log('C1');
await nextTick();
console.log('D1');
}
// 方案B:单次await
async function methodB() {
console.log('A2');
await nextTick();
console.log('B2');
console.log('C2');
console.log('D2');
}
methodA();
methodB();
// 连续调用输出结果
A1
A2
B1
B2
C2
D2
C1
D1
执行流程示意图:
【三次 await 的微任务拆分】
主线程: A
↓
微任务1: [nextTick回调] → B
↓
微任务2: [nextTick回调] → C
↓
微任务3: [nextTick回调] → D
↓
事件循环继续
【单次 await 的微任务合并】
主线程: A
↓
微任务1: [nextTick回调] → B → C → D
↓
事件循环继续
关键差异:
- 三次await:在 B、C、D 之间 允许其他微任务插队(如Promise.then)
- 单次await:B、C、D 连续执行,中间不会被其他微任务打断
2、实际场景
场景1:Vue DOM 更新后操作
// 场景:连续更新数据后获取DOM
// 方法一:三次 await this.$nextTick()
this.count = 1;
await this.$nextTick(); // 等待第一次DOM更新
console.log('第一次:', this.$el.textContent);
this.count = 2;
await this.$nextTick(); // 等待第二次DOM更新
console.log('第二次:', this.$el.textContent);
this.count = 3;
await this.$nextTick(); // 等待第三次DOM更新
console.log('第三次:', this.$el.textContent);
// 输出可能只有最后一次更新,因为Vue批量处理!
// 方法二:单次 await this.$nextTick()
this.count = 1;
this.count = 2;
this.count = 3;
await this.$nextTick(); // 批量等待所有更新
console.log('最终:', this.$el.textContent); // 看到最终结果
Vue批量更新示意图
【三次 await】
数据1 → 数据2 → 数据3 → [Vue批量队列] → nextTick1 → nextTick2 → nextTick3
↓ ↓ ↓
【Promise已resolve,三次都是同一个结果】
【单次 await】
数据1 → 数据2 → 数据3 → [Vue批量队列] → await nextTick
↓
【批量更新后一次性获取】
方法一 三次 调用的结果完全一样。
结论:在Vue中,多次 await nextTick() 通常是无意义的,因为同一事件循环内的多次调用返回的是 同一个Promise。所以它们都等待的是 同一个批处理更新完成后的 最终DOM状态。
Vue nextTick 内部机制:
// Vue 3 nextTick 简化实现
let currentFlushPromise = null
function nextTick(fn) {
const p = currentFlushPromise || (currentFlushPromise = Promise.resolve().then(flushJobs))
return fn ? p.then(fn) : p
}
function flushJobs() {
// 批量执行所有队列中的更新
// ...
currentFlushPromise = null // 重置
}
关键:
- currentFlushPromise 缓存机制:第一次调用时创建 Promise,后续调用复用同一个
- 所有更新在 flushJobs 中 批量处理,DOM只更新一次
- 三个 await 等待的是 同一个微任务 的完成
执行过程示意图
时间线: ────────────────────────────────────────────────────→
主线程 │ 微任务队列执行
│
this.count = 1 │
this.count = 2 │
this.count = 3 │
↓
await nextTick() ─────┬──→ p1 = currentFlushPromise
await nextTick() ─────┼──→ p2 = p1 (同一个)
await nextTick() ─────┼──→ p3 = p1 (同一个)
console.log('...') │ │
│ ↓
│ flushJobs() 批量更新DOM (count=3)
│ p1.resolve() → p2.resolve() → p3.resolve()
│ │
└─→ └─→ 三个await同时继续,看到相同结果
DOM状态: │ 只更新一次 → 最终状态 count=3
// 实际测试代码
this.count = 1;
const p1 = this.$nextTick();
this.count = 2;
const p2 = this.$nextTick();
this.count = 3;
const p3 = this.$nextTick();
console.log(p1 === p2); // true 是同一个Promise!
console.log(p2 === p3); // true
await p1; // 看到 count=3
await p2; // 看到 count=3 (和p1结果完全相同)
await p3; // 看到 count=3 (和p1结果完全相同)
总结
| 调用方式 | 返回的Promise | DOM更新次数 | 看到的结果 |
|---|---|---|---|
| 三次 await this.$nextTick() | 同一个实例 | 1 次(批量) | 三次都是最终状态 |
| 三次 process.nextTick() (Node.js) | 不同实例 | N/A | 取决于回调内部逻辑 |
- 功能上等价:三次调用 ≈ 一次调用(因为返回相同Promise)
- 语义上错误:代码可读性差,让人误以为能看到中间态
- 性能浪费:创建3次Promise包装,但底层只执行1次调度
正确做法:
// 只需一次await
this.count = 1;
this.count = 2;
this.count = 3;
await this.$nextTick(); // 等待批量更新完成
console.log('最终:', this.$el.textContent); // "3"
场景2:Node.js 原生 process.nextTick()
// Node.js 原生回调风格
// 方案A:三次调用
process.nextTick(() => console.log('tick1'));
process.nextTick(() => console.log('tick2'));
process.nextTick(() => console.log('tick3'));
// 方案B:单次调用
process.nextTick(() => {
console.log('tick1');
console.log('tick2');
console.log('tick3');
});
// 执行顺序对比(假设有Promise插入):
Promise.resolve().then(() => console.log('promise'));
// 方案A输出:
// tick1 → tick2 → tick3 → promise
// 微任务队列会全部执行完
// 方案B输出:
// tick1 → tick2 → tick3 → promise
// 结果相同,但机制不同
微任务队列详细过程:
【三次独立调用】
微任务队列: [callback1, callback2, callback3]
执行: callback1 → callback2 → callback3
(全部清空后才执行下一个阶段)
【单次调用】
微任务队列: [callback]
执行: callback(内部执行3个console)
(效果相同,但内存占用和调用栈不同)
3、核心结论
在 Vue 中:await this.$nextTick() 同一事件循环内多次调用是幂等的,返回相同 Promise。只需调用一次等待批量更新。
在 Node.js 中:三次 process.nextTick() 调用会创建 三个独立的微任务,虽然执行顺序连续,但增加了微任务队列长度,在极端高频场景下可能 阻塞I/O。
功能不等价:三次await提供了更细粒度的控制,但99%的场景下单次await+内部逻辑是更好的选择。
推荐写法:
// 推荐:单次await,内部处理所有逻辑
async function bestPractice() {
// 批量数据更新
data.a = 1;
data.b = 2;
data.c = 3;
// 单次等待
await nextTick(); // 或 Vue的this.$nextTick()
// 后续操作
console.log('全部完成');
}
// 避免:无意义的多次await(除非有特殊中断需求)
async function antiPattern() {
data.a = 1;
await nextTick(); // 多余
data.b = 2;
await nextTick(); // 多余
}
Vue DOM 更新 & 浏览器渲染 & 各类任务时机

可以看到:
nextTick → 在 DOM patch 后、渲染前
rAF → 在渲染前一刻
setTimeout → 在渲染完成后一段时间
这三者完全不处在同一“楼层”,所以行为不同。
更多推荐


所有评论(0)