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

宏任务大家族:不止 setTimeout,而且它们并不等价

「宏任务 (macrotask / task)」是事件循环一次只取一个的那类任务。最常见的是 setTimeout,但它远非全部:MessageChannelpostMessagesetInterval、各种 Event、网络回调 (XHR / fetch / WebSocket) 都会向宏任务队列提交任务。更关键的是——它们并不等价:setTimeout(,0)setTimeout(\dots , 0) 实际有最小延时会被限流,而 MessageChannel 几乎是「立即」的宏任务。本页全部在真实浏览器中执行。

1 · 五任务竞速:同时排入,谁先执行?

下面在同一段同步代码中一次性排入五种任务,然后记录它们实际执行的先后顺序。多点击几次「执行」:

几乎每次都是:两个微任务最先 (Promise.then / queueMicrotask) → 然后是 MessageChannel / postMessage 这类「即时宏任务」→ setTimeout(0) 反而靠后(它被限流了)→ rAF 要等到下一次渲染机会。这说明:宏任务之间也有快慢

2 · setTimeout(…, 0) 真的是 0ms 吗?

规范规定:当 setTimeout 嵌套超过 5 层(定时器里再设定时器…),最小延时被钳到 4ms (clamping)。下面连环嵌套 12 次 setTimeout(,0)setTimeout(\dots , 0),实测每一跳的真实间隔:

所以「setTimeout(fn, 0) 让出主线程、稍后执行」是对的,但「立即执行」是错的——它至少要等当前任务 + 所有微任务 + 可能的 4ms。需要尽可能快的宏任务时,业界使用 MessageChannel(见下)。后台标签setTimeout / setInterval 还会被进一步限流到 ≥1000ms 以节省电量。

3 · 这一家各自的用途

「跨上下文」= 能把消息送到另一个 window / iframe / Worker。其余都只在当前上下文排队。
宏任务来源 怎么被触发 后台标签限流 跨上下文 典型用途
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 宏任务收到。它是事件驱动、天生异步——这也是为什么它是宏任务而不是微任务。