Skip to content

TL;DR(结论先行)

  • 一个“浏览器帧”的典型顺序(简化):

    1. Task(宏任务)
    2. Microtasks(微任务检查点)Promise.then/queueMicrotask
    3. requestAnimationFrame 回调
    4. Microtasks(再次检查点)
    5. 渲染机会(样式计算/布局/绘制/合成)这里“可能”发生绘制
    6. requestIdleCallback(若本帧有空闲)
  • 可能渲染”的原因:浏览器有权跳过这一帧的绘制(无可见变更/后台标签页/性能节流/被连续微任务“饿死”等)。

  • Vue 3 把更新批量安排在微任务里;nextTick 的回调会在本轮 DOM patch 完成之后执行,仍属于微任务阶段。


为什么是“可能发生一次绘制”?

浏览器每一帧只有一个“渲染机会”,但是否真的去绘制,取决于以下条件:

  1. 没有可见变化:样式/布局/像素都没变 → 跳过绘制。
  2. 页面不可见/后台标签:根据 Page Visibility/节流策略,绘制频率被大幅降低甚至暂停。
  3. 节流与合成优化:有些变更(如仅合成层 transform/opacity)可由合成器线程处理,不一定每帧主线程都要完整“样式 → 布局 → 绘制”。
  4. 主线程被占用:若你在一帧中不断塞入微任务,可能导致微任务饥饿,渲染时机被一再推迟。
  5. 浏览器合帧/省电策略:屏幕刷新率/系统负载/节能模式下,浏览器可主动减少绘制帧数。

所以规范里描述为“update the rendering(更新渲染)”,这一步提供绘制的机会,但不保证一定输出一帧像素。


事件循环——标准顺序图(可记忆版本)

一帧内的关键阶段:

[1] 宏任务(Task)      例如:setTimeout 回调、事件回调、脚本执行入口
[2] 微任务检查点        例如:Promise.then / queueMicrotask
[3] requestAnimationFrame 回调(可见文档)
[4] 微任务检查点        一些实现会在 rAF 后再跑一次微任务,清理尾巴
[5] 更新渲染(样式/布局/绘制/合成)← 可能绘制,也可能跳过
[6] requestIdleCallback(若本帧还有空闲时间)
→ 下一帧或下一个 Task

与三个常见 API 的关系:

  • setTimeout(cb, 0):排入 [1] 宏任务(下一轮 Task)。
  • Promise.then / queueMicrotask:排入 [2]/[4] 微任务(本轮 Task 结束立刻执行,渲染前)。
  • requestAnimationFrame:发生在 [3]微任务之后、渲染之前

Vue 3 的渲染时机与 nextTick 在哪?

Vue 3 的核心点:批处理 + 微任务调度。

  • 当你修改响应式数据时,Vue 不会立刻渲染,而是把渲染任务放进一个队列;
  • 这个队列的刷新(flushJobs)被安排在微任务中(通常通过 Promise.resolve());
  • DOM patch(渲染)发生在这个微任务的 flush 过程中
  • nextTick 会把你的回调排在渲染 flush 之后的微任务里,因此能读到更新后的 DOM

把 Vue 放进整体时间线(单帧):

同步代码(你的事件处理 / 方法调用)
  └─ 触发响应式变更:Vue 把渲染任务加入队列(不立即渲染)
Task 结束

Microtasks 检查点:
  ├─ Vue flushJobs:执行计算/patch → 更新真实 DOM
  └─ Vue 的 nextTick 回调(在 flush 之后)→ 你读到“已更新”的 DOM

requestAnimationFrame

(再次 Microtasks 检查点,视实现而定)

渲染机会(可能绘制)

这也解释了:为什么 await nextTick() 后读取 DOM 是安全的——因为 Vue 的 patch 已在同一轮微任务更早阶段完成。


queueMicrotask / Promise.then / setTimeout 的位次对比

  • queueMicrotaskPromise.then:同属微任务,发生在Task 结束后、rAF 与渲染之前

    • 差别在于语义与开销:queueMicrotask 更轻,不引入 Promise 链;then 便于链式处理。
  • setTimeout:下一轮 Task,发生在渲染机会之后(通常会比 microtask 晚一拍甚至一整帧)。

这就是在 i18n 初始化里优先用 queueMicrotask的原因: 不阻塞首屏,但尽可能早(在本轮渲染前)触发语言包懒加载与切换,缩短“临时英文 → 目标语言”的可见窗口。


常见疑惑与例外

1) 为什么我改了样式却没立刻看到?

可能落在以下情况:

  • 改动还在微任务阶段,浏览器尚未进入渲染机会
  • 改动只影响合成层,由合成器线程合成,不会每次都触发布局/绘制
  • 页面在后台或被节流,这一帧被跳过

2) 连续大量微任务会怎样?

  • 可能造成微任务饥饿(一直在跑微任务,导致 rAF/渲染迟迟不到);
  • 解决:在密集计算中分片setTimeout/scheduler.postTask/requestIdleCallback)或使用 Web Worker。

3) nextTickrequestAnimationFrame 谁先?

  • nextTick(微任务),后 rAF(渲染前阶段)
  • 想在真正绘制完成后读取(例如截图/像素检测),考虑在 rAF 内再排一个 setTimeout(0)(下一轮 Task),或使用 await paint 类的策略(需自定义同步点)。

开发中怎么用这套顺序做判断?

  • 读更新后的 DOMawait nextTick()(Vue)或确保逻辑在微任务里执行。
  • 尽快触发但不阻塞:用 queueMicrotask
  • 让出一帧,避免卡顿:用 setTimeout(0)requestIdleCallback(有空闲才跑)。
  • 和绘制对齐:用 requestAnimationFrame,它在渲染前回调里给你最后时刻的控制权。

口袋卡片(Cheat Sheet)

  • 帧序:Task → Microtasks → rAF → Microtasks → Render(可能) → Idle → Next Task
  • 微任务:Promise.then / queueMicrotask / Vue 的 flush 与 nextTick
  • 宏任务:setTimeout / 事件回调 / 网络回调
  • “可能”渲染:无可见变化、后台、节流、合成优化、微任务饥饿,都可能跳过
  • Vue:更新合并 → 微任务阶段 flush → DOM patch 完成 → nextTick 回调 → rAF → 绘制

一句话总结

把“微任务在前、渲染在后”的大框架记住:Vue 的 DOM 更新发生在微任务的 flush 中,nextTick 也在微任务中但晚于 patch;rAF 在渲染前;真正绘制可能发生,也可能被跳过。掌握这个顺序,你就知道该把哪段代码放在哪个阶段,以换来更稳的时序与更快的体验。

本站总访问