Web 平台 API / 事件循环 · 任务、microtask,与那一圈的所有时机 / macrotask 大家族:不止 setTimeout,而且它们并不等价 待审核 2 / 6
macrotask · setTimeout 之外还有谁

macrotask 大家族:不止 setTimeout,而且它们并不等价

「macrotask (macrotask / task)」是事件循环一次只取一个的那类任务。最常见的是 setTimeout,但它远非全部:MessageChannelpostMessagesetInterval、各种 Event、网络回调 (XHR / fetch / WebSocket) 都会向 macrotask 队列提交任务。更关键的是——它们并不等价setTimeout(fn, 0) 在连环嵌套超过五层后会被钳到 4ms,而 MessageChannel 没有这道下限。本页全部在真实浏览器中执行。

1 · 五任务竞速:同时排入时的实际顺序

下面在同一段同步代码中一次性排入五种任务,记录它们实际执行的先后顺序

图 1-1 · 五种任务同时排入后的实际执行顺序。可反复执行,观察名次是否稳定。

注 · 稳定的部分只有一条:两个 microtask 最先 (Promise.then / queueMicrotask)。三个 macrotask 的相对名次并不固定——首次排入的 setTimeout(0) 尚未触发钳制,实测 Chrome 151 连跑 8 次,其中 6 次它排在 MessageChannel / postMessage 之前(核对于 2026-08)。rAF 落在何处取决于本次是否正好赶上渲染机会。macrotask 之间的快慢差异要到连环嵌套之后才稳定显现,见 §2。

2 · setTimeout 的最小延时与 4ms 钳制

HTML 规范规定:当 setTimeout嵌套层数超过 5(定时器里再设定时器)且请求的延时小于 4ms 时,延时被钳到 4ms (clamping)。下面连环嵌套 12 次 setTimeout(fn, 0),实测每一跳的真实间隔——前六跳在 0.1ms 量级,第七跳起稳定在 4.5ms 左右(Chrome 151,核对于 2026-08)。

图 2-1 · 连环嵌套 12 次 setTimeout 的逐跳真实间隔,前六跳与其后的台阶差即 4ms 钳制。

警示 ·setTimeout(fn, 0) 让出主线程、稍后执行」是对的,但「立即执行」是错的——它至少要等当前任务与其后的全部 microtask,连环嵌套时还要再加 4ms。需要大量连续让出时,业界使用 MessageChannel(见下)。后台标签setTimeout / setInterval 会被限流到 1 秒一次;页面隐藏超过 5 分钟、静音超过 30 秒且嵌套层数达到 5 时,Chrome 88 起进一步降到每分钟一次(核对于 2026-08)。

3 · 这一家各自的用途

「跨上下文」= 能把消息送到另一个 window / iframe / Worker。其余都只在当前上下文排队。
macrotask 来源 怎么被触发 后台标签限流 跨上下文 典型用途
setTimeout / setInterval 时间驱动 (到点) 是 (≥1s) 延时、轮询、防抖节流
MessageChannel 消息驱动 (port 投递) 否 (同上下文两端口) 「尽快」的 macrotask、React 调度器
postMessage 消息驱动 ✓ iframe / window / worker 跨上下文通信 (IPC),为此而生
Event (click / load…) 用户 / 浏览器派发 交互、生命周期
XHR / fetch / WebSocket I/O 完成回调 网络回包后续处理
Scheduler.postTask() 带优先级调度 规范未规定 现代任务调度 (第 5 页

注 · MessageChannel 为何不被限流。它本是为跨上下文消息设计的(一对 port,一端 postMessage、另一端 onmessage)。但实践中发现:把消息发给自身,就得到一个不受定时器钳制、后台也不降速的 macrotask——恰好满足调度器的需求(Chrome 的定时器限流只作用于 setTimeout / setInterval)。React 的 Scheduler 早期即以 MessageChannel 实现时间切片的 yield(在应用页详述)。

注 · postMessage 的本职是 IPC。主页面 ↔ <iframe>、主线程 ↔ Worker 之间不共享内存,只能传消息postMessage 把数据(结构化克隆)投到对方的事件队列,对方以一个 message macrotask 收到。它是事件驱动、天生异步——这也是为什么它是 macrotask 而不是 microtask。