关键在于理解“执行时机”和“微任务队列清空逻辑”:当前宏任务结束后持续执行微任务直至队列为空,且新微任务会立即加入本轮清空;mutationobserver、queuemicrotask、promise 链及 await 均触发微任务;浏览器渲染在宏任务+微任务后、下一轮宏任务前;node.js 中 process.nexttick 优先级高于所有微任务。

掌握宏任务与微任务的高级进阶技巧,关键不在死记队列类型,而在于理解“执行时机”和“队列清空逻辑”这两个底层动作。只要抓住这两点,复杂嵌套、递归添加、跨环境差异都能自然推导出来。
真正搞懂“微任务队列清空”这一步
很多人以为“执行一个微任务后就去检查宏任务”,其实不是——事件循环会在**当前宏任务结束后,持续执行微任务,直到微任务队列彻底为空**。这个过程是递归的:哪怕在 Promise.then 里又 new 一个 Promise 或调用 queueMicrotask,新微任务也会被立刻加入当前微任务队列,并在本轮清空中执行。
- 例如:
Promise.resolve().then(() => { console.log(1); Promise.resolve().then(() => console.log(2)); });输出一定是 1 → 2,中间不会插入任何宏任务 - 再比如:
queueMicrotask(() => { console.log('a'); queueMicrotask(() => console.log('b')); });也一定是 a → b - 错误认知:“微任务只执行一次”;正确理解:“微任务队列必须榨干”
识别哪些操作会“当场触发”新微任务
某些 API 表面不显眼,但内部会同步注册微任务,容易成为性能陷阱或执行顺序误判源:
将一个或多个视频上传至YouTube。当用户想“上传到YouTube”“发YouTube”“批量上传”“upload to YouTube”“post videos to YouTube”或发布到YouTube时使用此技能。
-
MutationObserver:监听 DOM 变更,回调是微任务;哪怕只改一个 class,observer 回调也排在所有 Promise.then 之后、下一个 setTimeout 之前 -
queueMicrotask(fn):现代标准 API,语义清晰,比 Promise.resolve().then(fn) 更轻量,无 Promise 构造开销 -
Promise.reject().catch()和finally()同样进入微任务队列,和 then 完全平级 - 注意:
await后续代码本质是 Promise.then 的语法糖,所以也属于微任务链
浏览器渲染与宏任务的隐式绑定
在浏览器中,UI 渲染本身就是一个宏任务(虽然你没手动调用),而且它有固定位置:在**一轮宏任务执行完 + 所有微任务清空后,且下一轮宏任务开始前**,浏览器会主动插入渲染帧。这意味着:
-
setTimeout(() => {}, 0)的回调总是在渲染之后执行(除非被更高优先级宏任务抢占) -
requestAnimationFrame(cb)是宏任务,但它的执行时机特殊:在下一帧绘制前、当前宏任务+微任务之后,因此常用来做动画对齐 - 想强制触发重排/重绘并观察效果?得用
setTimeout包一层,否则 DOM 修改 + 微任务更新可能全被压在单帧里,肉眼看不到中间态
Node.js 与浏览器的微任务优先级差异
Node.js 中 process.nextTick 比所有 Promise.then 和 queueMicrotask 优先级都高——它甚至在当前操作完成、但还没退出当前事件循环阶段时就执行。这是 Node.js 特有的调度策略,用于保证 I/O 错误处理及时性:
- 代码示例:
process.nextTick(() => console.log('nextTick')); Promise.resolve().then(() => console.log('promise'));输出一定是 nextTick → promise - 浏览器没有 process.nextTick,但可以用
queueMicrotask替代大部分场景 - 跨平台库若需兼容,应避免依赖 nextTick 的超高优先级,改用 Promise 链或显式 microtask 调度










