macrotask 大家族:不止 setTimeout,而且它们并不等价
「macrotask (macrotask / task)」是事件循环一次只取一个的那类任务。最常见的是 setTimeout,但它远非全部:MessageChannel、postMessage、setInterval、各种 Event、网络回调 (XHR / fetch /
WebSocket) 都会向 macrotask 队列提交任务。更关键的是——它们并不等价:setTimeout(fn, 0) 在连环嵌套超过五层后会被钳到 4ms,而 MessageChannel 没有这道下限。本页全部在真实浏览器中执行。
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)。
警示 ·「setTimeout(fn, 0) 让出主线程、稍后执行」是对的,但「立即执行」是错的——它至少要等当前任务与其后的全部 microtask,连环嵌套时还要再加 4ms。需要大量连续让出时,业界使用 MessageChannel(见下)。后台标签中
setTimeout / setInterval 会被限流到 1 秒一次;页面隐藏超过 5 分钟、静音超过 30 秒且嵌套层数达到 5 时,Chrome 88 起进一步降到每分钟一次(核对于 2026-08)。
3 · 这一家各自的用途
| 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。