事件循环 · 任务、微任务,与那一圈的所有时机
JavaScript 是单线程的,却要同时响应点击、运行定时器、等待网络——靠的是一个持续循环的 事件循环 (event loop)。它的规则可以概括为一句:取一个宏任务执行完 → 把微任务全部清空 → 到时机就渲染 → 再取下一个。 理解这一圈,就能解释「为什么 Promise.then 比 setTimeout(0) 先执行」「页面为什么卡顿」「Vue/React 为何不卡」。
本系列从这一圈本身出发,逐个拆解向队列提交任务的入口: 骨架 → 宏任务家族 → 微任务 → rAF/rIC → postTask → 落地。每页都在真实浏览器中执行, 单步观察队列的增删,并直观对比卡顿与流畅的差别。
一圈是怎么转的:同步 → 微任务 → 渲染 → 下一个宏任务
把整段 <script> 视为「第一个宏任务」,单步操作一个可前进 / 后退的事件循环模拟器:观察 setTimeout 进入宏任务队列、Promise.then / queueMicrotask 进入微任务队列,以及引擎如何「清空微任务 → 渲染 → 取下一个宏任务」。四个示例(经典顺序题 / 微任务中再生微任务 / 两个定时器各带微任务 / 加入 rAF)说明为什么 Promise.then 总在 setTimeout(0) 之前执行。
宏任务大家族:不止 setTimeout,而且它们并不等价
在真实浏览器中执行:同时排入 setTimeout / MessageChannel / postMessage / Promise / rAF,记录执行先后;实测 setTimeout(… , 0) 嵌套超过 5 层后被钳制到 4ms 的台阶。说明 MessageChannel 为何是「不被限流的即时宏任务」(React 调度器的基础)、postMessage 的本职是跨上下文 IPC,以及 Event / XHR / fetch / WebSocket 这些宏任务来源。
微任务:在「下一个宏任务」之前全部执行完
三个真实 demo 演示微任务的三个特性:优先执行(5 连环微任务全部执行完,setTimeout 才轮到)、批处理(MutationObserver 同步修改 N 次 DOM,回调只合并为 1 次)、可能阻塞渲染(一条长微任务链让计数器从 0 直接跳到终点,中途不渲染任何一帧——microtask starvation)。并说明 await 之后的代码即一个微任务。
两个特殊任务:渲染前的 rAF,空闲时的 rIC
把一帧拆开,看 requestAnimationFrame(绘制前执行,动画的正确工具)与 requestIdleCallback(绘制后、空闲时执行)各自所处的时机。实时 rAF 动画 + FPS 表(切到后台观察限流暂停)、四种任务的执行顺序、用 deadline.timeRemaining() 把大任务切分到各帧的空隙,以及实测 rAF 的排队时机:事件回调里排 = 同帧、rAF 里排 = 下一帧,且同帧回调拿到完全相同的 timestamp。
Scheduler.postTask:给宏任务装上「优先级」
setTimeout 没有优先级,埋点和响应输入同等对待。scheduler.postTask 为每个任务提供三档优先级 (user-blocking / user-visible / background):乱序排入、按优先级执行(同档 FIFO);配合 TaskController 可动态调整优先级 / 取消。与 setTimeout / rIC 逐项对比,再介绍时间切片的现代配套 scheduler.yield()。(不支持的浏览器使用等价模拟。)
落地:框架与性能优化,本质都是在与事件循环打交道
直观对比长任务切片:同样 800ms 的工作,一次性执行会冻结转盘、无法点击按钮、卡顿帧激增;切片 + 让出主线程则全程响应。再串起 Vue nextTick(一个微任务批量更新)、React Scheduler(MessageChannel 时间切片)、scroll 用 rAF 节流,以及一张「需求与方案对照」速查表。落到现代指标 INP:避免长任务阻塞在交互和下一次绘制之间。
一句话串起来
rAF / rIC 位于渲染前后两个特殊时机 (见 rAF / rIC);postTask 为宏任务补上了优先级 (见 Scheduler.postTask)。 工程上所有关于卡顿与流畅的取舍 (见 落地),本质都是「把代码放在这一圈的哪个时机、是否按时让出主线程」。相关链接
- HTML Standard · Event loops html.spec.whatwg.org 事件循环的规范定义:task queues / microtask checkpoint / rendering 的「processing model」原文。
- MDN · JavaScript execution model developer.mozilla.org 从执行模型角度概述 job queue / event loop、run-to-completion 与 task / microtask 的优先级划分。
- Jake Archibald · In The Loop youtube.com (JSConf) 本系列参考来源之一:用动画清晰讲解 tasks / microtasks / rAF 的顺序,以及 microtask 阻塞渲染 (starvation)。
- Paul Irish · requestAnimationFrame scheduling for nerds medium.com rAF 的排队细节:event handler 里排 = 同帧、rAF 里排 = 下一帧;同帧回调共享同一个 frame timestamp;rAF 不自动限流 / coalesce 靠自己。对应 rAF / rIC 页的 rAF 排队一节。
-
MDN · Using microtasks in JavaScript
developer.mozilla.org
queueMicrotask指南:何时用微任务、与 Promise / setTimeout 的取舍。 -
MDN · Scheduler.postTask()
developer.mozilla.org
三档优先级、TaskController、与
scheduler.yield();Scheduling APIs 的入口。 -
Can I use · Scheduler.postTask
caniuse.com
scheduler.postTask的实时浏览器支持矩阵 (Safari 尚未支持,需等价模拟)。 - MDN · Window.requestAnimationFrame() developer.mozilla.org 在下一次绘制前执行回调;timestamp 参数、后台标签暂停、one-shot 需逐帧续排。
- web.dev · Optimize long tasks web.dev 长任务为何伤 INP、yield to main 的几种写法 (postTask / scheduler.yield / 退路),以及 isInputPending。
-
MDN · requestIdleCallback()
developer.mozilla.org
空闲回调的
deadline.timeRemaining()/didTimeout/timeout选项与兼容性。