← 首页 / 事件循环 · 任务、微任务,与那一圈的所有时机 待审核 6 页

事件循环 · 任务、微任务,与那一圈的所有时机

JavaScript 是单线程的,却要同时响应点击、运行定时器、等待网络——靠的是一个持续循环事件循环 (event loop)。它的规则可以概括为一句:取一个宏任务执行完 → 把微任务全部清空 → 到时机就渲染 → 再取下一个。 理解这一圈,就能解释「为什么 Promise.thensetTimeout(0) 先执行」「页面为什么卡顿」「Vue/React 为何不卡」。

本系列从这一圈本身出发,逐个拆解向队列提交任务的入口: 骨架 → 宏任务家族 → 微任务 → rAF/rIC → postTask → 落地。每页都在真实浏览器中执行, 单步观察队列的增删,并直观对比卡顿与流畅的差别。

先理解这一圈 the loop · 调用栈 + 宏任务 + 微任务

一圈是怎么转的:同步 → 微任务 → 渲染 → 下一个宏任务

把整段 <script> 视为「第一个宏任务」,单步操作一个可前进 / 后退的事件循环模拟器:观察 setTimeout 进入宏任务队列、Promise.then / queueMicrotask 进入微任务队列,以及引擎如何「清空微任务 → 渲染 → 取下一个宏任务」。四个示例(经典顺序题 / 微任务中再生微任务 / 两个定时器各带微任务 / 加入 rAF)说明为什么 Promise.then 总在 setTimeout(0) 之前执行。

逐个拆解向队列提交任务的入口 macrotask · setTimeout 之外还有谁

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

真实浏览器中执行:同时排入 setTimeout / MessageChannel / postMessage / Promise / rAF,记录执行先后;实测 setTimeout(… , 0) 嵌套超过 5 层后被钳制到 4ms 的台阶。说明 MessageChannel 为何是「不被限流的即时宏任务」(React 调度器的基础)、postMessage 的本职是跨上下文 IPC,以及 Event / XHR / fetch / WebSocket 这些宏任务来源。

microtask · 高优先级的小任务

微任务:在「下一个宏任务」之前全部执行完

三个真实 demo 演示微任务的三个特性:优先执行(5 连环微任务全部执行完,setTimeout 才轮到)、批处理MutationObserver 同步修改 N 次 DOM,回调只合并为 1 次)、可能阻塞渲染(一条长微任务链让计数器从 0 直接跳到终点,中途不渲染任何一帧——microtask starvation)。并说明 await 之后的代码即一个微任务。

rAF / rIC · 跟着画面走

两个特殊任务:渲染前的 rAF,空闲时的 rIC

一帧拆开,看 requestAnimationFrame(绘制执行,动画的正确工具)与 requestIdleCallback(绘制、空闲时执行)各自所处的时机。实时 rAF 动画 + FPS 表(切到后台观察限流暂停)、四种任务的执行顺序、用 deadline.timeRemaining() 把大任务切分到各帧的空隙,以及实测 rAF 的排队时机:事件回调里排 = 同帧、rAF 里排 = 下一帧,且同帧回调拿到完全相同的 timestamp

scheduler.postTask · 带优先级的宏任务

Scheduler.postTask:给宏任务装上「优先级」

setTimeout 没有优先级,埋点和响应输入同等对待。scheduler.postTask 为每个任务提供三档优先级 (user-blocking / user-visible / background):乱序排入、按优先级执行(同档 FIFO);配合 TaskController动态调整优先级 / 取消。与 setTimeout / rIC 逐项对比,再介绍时间切片的现代配套 scheduler.yield()。(不支持的浏览器使用等价模拟。)

落地:框架与性能,都取决于时机的选择 applications · 框架与工程里的事件循环

落地:框架与性能优化,本质都是在与事件循环打交道

直观对比长任务切片:同样 800ms 的工作,一次性执行会冻结转盘、无法点击按钮、卡顿帧激增;切片 + 让出主线程则全程响应。再串起 Vue nextTick(一个微任务批量更新)、React Scheduler(MessageChannel 时间切片)、scroll 用 rAF 节流,以及一张「需求与方案对照」速查表。落到现代指标 INP:避免长任务阻塞在交互和下一次绘制之间。

一句话串起来

主线程只有一个,事件循环按「宏任务 → 微任务清空 → 渲染 (rAF) → 空闲 (rIC)」逐圈运转 (见 先理解这一圈)。 向队列提交任务的入口分两类:宏任务一次取一个 (见 宏任务大家族,且彼此有快慢之分),微任务一次清空到底 (见 微任务,会优先执行、会批处理、可能阻塞渲染); rAF / rIC 位于渲染前后两个特殊时机 (见 rAF / rIC);postTask 为宏任务补上了优先级 (见 Scheduler.postTask)。 工程上所有关于卡顿与流畅的取舍 (见 落地),本质都是「把代码放在这一圈的哪个时机、是否按时让出主线程」

相关链接