同步、微任务、宏任务与 rAF:一次讲清事件循环调度
浏览器主线程里,「现在跑」和「稍后跑」不是一回事。同步代码、微任务、宏任务、requestAnimationFrame 分属不同调度层。本文用一张心智模型把它们对齐,并附上可复现的控制台实验与实测结果——尤其是 rAF 与 setTimeout(0) 谁先谁后并不保证。
一句话总览
事件循环大致按这个节奏转:同步跑完 → 清空微任务队列 →(若该画了)样式/布局 → rAF → 绘制 → 再取下一个宏任务。不同环境细节会有出入,但用来选 API、排 bug 已经够用。
四类任务分别干什么
1. 同步任务(Sync)
当前调用栈上直接执行的代码。立刻跑、不排队;跑完之前任何异步回调都进不来。太长会卡死页面。
2. 微任务(Microtask)
当前同步结束后、渲染之前要清空的一队回调。常见来源:queueMicrotask、Promise.then/catch/finally(含 async/await 续体)、MutationObserver。同步一结束就跑,且会一口气清空整队;比 setTimeout(0) 早。适合尽快续上逻辑;不适合大计算或必须先画一帧的事。
3. 宏任务(Macrotask)
事件循环里「下一轮任务」的队列。常见来源:setTimeout / setInterval、用户事件、网络完成、MessageChannel 等。比微任务晚;宏任务之间浏览器有机会渲染和处理输入。setTimeout(0) 也不是真 0ms。适合延后执行、拆长任务;不要用它精确对齐动画帧。
4. requestAnimationFrame(rAF)
登记在下一帧绘制之前跑的回调,跟屏幕刷新对齐。为视觉更新服务;后台标签页常被暂停或降频。与 setTimeout(0) 谁先谁后不保证。
实验:把四者放进同一段代码
在浏览器控制台直接粘贴运行:
1console.log('1 sync')23queueMicrotask(() => console.log('2 micro'))45Promise.resolve().then(() => console.log('3 promise micro'))67setTimeout(() => console.log('5 macro'), 0)89requestAnimationFrame(() => console.log('4 rAF'))1011console.log('1b sync end')
实测输出
某次在浏览器控制台中的实际结果:
1// 实际控制台输出:2// 1 sync3// 1b sync end4// 2 micro5// 3 promise micro6// undefined // 控制台对整段脚本的返回值,可忽略7// 5 macro8// 4 rAF
解读:
- 1 sync → 1b sync end:同步先全部跑完,铁律。
- 2 micro → 3 promise micro:同属微任务队列,先入先出;queueMicrotask 与 Promise.then 同档。
- 5 macro 出现在 4 rAF 之前:说明这次微任务清空后浏览器没有立刻进入渲染,先处理了 setTimeout(0),真正要画时才跑 rAF。
为什么 4 和 5 不一定
微任务清空后,浏览器会决定现在要不要画一帧。要画则可能先 rAF 再宏任务;先不画则可能先宏任务再 rAF(就是上面这份实测)。控制台、DevTools、标签是否在前台都会影响,因此不要把 rAF 与 setTimeout(0) 的先后写死。
可靠顺序 vs 不可靠顺序
- 同步 vs 一切异步:同步一定先完
- 微任务 vs 宏任务:微任务一定更早
- 微任务 vs rAF:一般微任务更早(在渲染前清空)
- rAF vs setTimeout(0):不一定
怎么选
- 现在立刻算完 → 同步(但别太久)
- 当前逻辑回合末尾马上续上 → queueMicrotask / Promise.then
- 故意推后 / 让出主线程 → setTimeout 等宏任务
- 跟下一帧绘制走 → requestAnimationFrame
- 写顺序断言 → 只断言同步 → 微任务;不要断言 rAF 与宏任务先后
同族 API 速查
- 微任务档:queueMicrotask、Promise.then、MutationObserver
- 帧 / 空闲:requestAnimationFrame、requestIdleCallback、ResizeObserver、IntersectionObserver
- 宏任务档:setTimeout、setInterval、MessageChannel、scheduler.postTask
- Node 特有:process.nextTick、setImmediate
小结
同步是正在干的活;微任务是刚收尾立刻补上的尾巴;rAF 是下一帧画画前的钩子;宏任务是下一轮事件循环的活。同步 → 微任务是铁律;rAF 与宏任务可以互换顺序,代码里别赌。把上面那段实验再跑几遍,会更容易记住「可靠」和「不可靠」的边界。
暂无评论,来抢沙发吧。