microtask:在「下一个 macrotask」之前全部执行完
microtask 是一类优先级更高的小任务:当前这段代码执行完(调用栈清空)后,引擎立即把 microtask 队列清空到底——清空过程中新产生的 microtask 也算这一轮,要一并清完——之后才会去渲染或取下一个
macrotask。页面代码常用的入口有三种:queueMicrotask()、Promise.then/catch/finally(含 await 之后的代码)、MutationObserver 的回调;此外 custom element 的回调反应、FinalizationRegistry 的清理回调也走 microtask
队列。本页全部真实执行,演示它们的三个特性:优先执行、批处理、可能阻塞渲染。
1 · microtask 全部先于 macrotask 执行
下面先排一个 setTimeout(一个 macrotask),再排一条 5 连环的 microtask 链(每个 microtask 中再排下一个)。看谁先执行完:
2 · MutationObserver:N 次同步改动合并为一次回调
MutationObserver 不是改一次 DOM 就回调一次。它把一轮同步代码里的所有改动攒成一批,在本轮 microtask 阶段一次性把所有记录交给回调——这正是「microtask 在 macrotask 边界统一结算」的体现。
3 · microtask 可能阻塞渲染 (starvation)
既然 microtask 必须清空到底才渲染,那么一条足够长的 microtask 链就能把渲染和 macrotask无限期推后。下面排一条 N 节的 microtask 链,每节都把屏幕上的计数器 +1 写入 DOM,同时排一个 setTimeout。
警示 · 计数器从 0 直接跳到终点,中途没有任何重绘——因为 microtask 未清空,浏览器没有渲染机会。如果这条链永不结束(例如 queueMicrotask 中无条件再调用
queueMicrotask),页面会完全冻结:点击、动画、乃至关闭标签页的提示都无响应。这是真实的故障模式,称为 microtask starvation。
注 · await x 相当于 Promise.resolve(x).then(后续代码)——await 之后的每一行都被打包成一个 microtask。因此 async 函数中一旦 await,后续逻辑就让到了 microtask 阶段,早于 setTimeout、早于渲染。queueMicrotask
则是「不需要 Promise,只需要一个 microtask 时机」的直接入口。