宏任务大家族:不止 setTimeout,而且它们并不等价
「宏任务 (macrotask / task)」是事件循环一次只取一个的那类任务。最常见的是 setTimeout,但它远非全部:MessageChannel、postMessage、setInterval、各种 Event、网络回调 (XHR / fetch /
WebSocket) 都会向宏任务队列提交任务。更关键的是——它们并不等价:
实际有最小延时会被限流,而 MessageChannel 几乎是「立即」的宏任务。本页全部在真实浏览器中执行。
1 · 五任务竞速:同时排入,谁先执行?
下面在同一段同步代码中一次性排入五种任务,然后记录它们实际执行的先后顺序。多点击几次「执行」:
几乎每次都是:两个微任务最先 (Promise.then / queueMicrotask) → 然后是 MessageChannel / postMessage 这类「即时宏任务」→ setTimeout(0) 反而靠后(它被限流了)→
rAF 要等到下一次渲染机会。这说明:宏任务之间也有快慢。
2 · setTimeout(…, 0) 真的是 0ms 吗?
规范规定:当 setTimeout 嵌套超过 5 层(定时器里再设定时器…),最小延时被钳到 4ms (clamping)。下面连环嵌套 12 次
,实测每一跳的真实间隔:
所以「setTimeout(fn, 0) 让出主线程、稍后执行」是对的,但「立即执行」是错的——它至少要等当前任务 + 所有微任务 + 可能的 4ms。需要尽可能快的宏任务时,业界使用 MessageChannel(见下)。后台标签中 setTimeout /
setInterval 还会被进一步限流到 ≥1000ms 以节省电量。
3 · 这一家各自的用途
| 宏任务来源 | 怎么被触发 | 后台标签限流 | 跨上下文 | 典型用途 |
|---|---|---|---|---|
| setTimeout / setInterval | 时间驱动 (到点) | 是 (≥1s) | 否 | 延时、轮询、防抖节流 |
| MessageChannel | 消息驱动 (port 投递) | 否 | 否 (同上下文两端口) | 「尽快」的宏任务、React 调度器 |
| postMessage | 消息驱动 | 否 | ✓ iframe / window / worker | 跨上下文通信 (IPC),为此而生 |
| Event (click / load…) | 用户 / 浏览器派发 | — | — | 交互、生命周期 |
| XHR / fetch / WebSocket | I/O 完成回调 | 否 | — | 网络回包后续处理 |
| Scheduler.postTask() | 带优先级调度 | 否 | 否 | 现代任务调度 (第 5 页) |
MessageChannel 为何更快且不被限流?它本是为跨上下文消息设计的(一对 port,一端 postMessage、另一端
onmessage)。但实践中发现:把消息发给自身,就得到一个「无最小延时、后台也不降速」的宏任务——恰好满足调度器的需求。React 的 Scheduler 早期即以 MessageChannel 实现时间切片的 yield(在应用页详述)。
postMessage 的本职是 IPC。主页面 ↔ <iframe>、主线程 ↔ Worker 之间不共享内存,只能传消息。postMessage 把数据(结构化克隆)投到对方的事件队列,对方以一个 message 宏任务收到。它是事件驱动、天生异步——这也是为什么它是宏任务而不是微任务。