Web 平台 API / 事件循环 · 任务、microtask,与那一圈的所有时机 / microtask:在「下一个 macrotask」之前全部执行完 待审核 3 / 6
microtask · 高优先级的小任务

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 中再排下一个)。看谁先执行完:

图 1-1 · 一个 setTimeout 与一条 5 连环 microtask 链同时排入后的实际执行顺序,逐条实时记录。

2 · MutationObserver:N 次同步改动合并为一次回调

MutationObserver 不是改一次 DOM 就回调一次。它把一轮同步代码里的所有改动攒成一批,在本轮 microtask 阶段一次性把所有记录交给回调——这正是「microtask 在 macrotask 边界统一结算」的体现。

图 2-1 · 同步连改 N 次 DOM 后 MutationObserver 的回调次数与每次交付的记录条数。可改动次数。

3 · microtask 可能阻塞渲染 (starvation)

既然 microtask 必须清空到底才渲染,那么一条足够长的 microtask 链就能把渲染和 macrotask无限期推后。下面排一条 N 节的 microtask 链,每节都把屏幕上的计数器 +1 写入 DOM,同时排一个 setTimeout

图 3-1 · 长 microtask 链与计数器重绘。可调链长,观察计数器是否出现中间值,以及同时排入的 setTimeout 何时才轮到。

警示 · 计数器从 0 直接跳到终点,中途没有任何重绘——因为 microtask 未清空,浏览器没有渲染机会。如果这条链永不结束(例如 queueMicrotask 中无条件再调用 queueMicrotask),页面会完全冻结:点击、动画、乃至关闭标签页的提示都无响应。这是真实的故障模式,称为 microtask starvation

注 · await x 相当于 Promise.resolve(x).then(后续代码)——await 之后的每一行都被打包成一个 microtask。因此 async 函数中一旦 await,后续逻辑就让到了 microtask 阶段,早于 setTimeout、早于渲染。queueMicrotask 则是「不需要 Promise,只需要一个 microtask 时机」的直接入口。