两次 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 → 在渲染完成后一段时间

这三者完全不处在同一“楼层”,所以行为不同。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐