← 事件循环 · 任务、微任务,与那一圈的所有时机 / 落地:框架与性能优化,本质都是在与事件循环打交道 待审核 6 / 6
applications · 框架与工程里的事件循环

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

前五页的机制并非纸上谈兵——你日常使用的框架、每条性能建议,底层都是在选取事件循环里的某个时机。本页把它们串联起来:Vue 用微任务批量更新React 用 MessageChannel 做时间切片滚动用 rAF 而非 setTimeout长任务需切碎并让出主线程以保住交互响应 (INP)。最后给出一张「需求与方案对照」速查表。

1 · 长任务会冻住页面——亲手感受切片

一段耗时约 800ms 的工作,放在主线程上一次性执行完,会阻塞渲染和点击;切成小块、块间让出主线程,页面便能持续响应。下面左侧的转盘由 rAF 驱动(理应持续转动),旁边的按钮统计可点击的次数——对比两种模式下的差别:

为什么用 MessageChannel 而不是 setTimeout(r, 0) 做退路?因为 setTimeout4ms 钳制,切几百次就会平白多耗约 1 秒;MessageChannel 无最小延时。这也是 React Scheduler 当年的选择。

2 · 框架里的事件循环

Vue 的 nextTick = 一个微任务

Vue 修改数据不会同步更新 DOM,而是把更新放入队列,用一个 microtask (Promise.then) 在当前同步代码执行完毕后一次性 flush。因此同一个 tick 里修改 100 次,只渲染 1 次(批处理,正是第 3 页的微任务结算)。await nextTick() 即「等待这个微任务执行完毕、DOM 已更新」。

React Scheduler = MessageChannel + 时间切片

React (Fiber) 把渲染拆成小单元,每执行约 5ms 就检查是否应当让出;让出靠 MessageChannel 排一个宏任务,把控制权交还给浏览器去处理输入和绘制,下一个宏任务再继续渲染。这使得大组件树更新时页面不会卡死——用的全是第 2 页讲的「不被限流的即时宏任务」。新版还在向 scheduler.postTask / isInputPending 迁移。

3 · 滚动 / 动画:rAF,不是 setTimeout

scroll / resize / mousemove 触发极为频繁,直接在事件里改样式会一帧修改多次、做无用功。正确做法是用 rAF 节流到每帧最多一次:事件里只记下最新值,在 rAF 回调里读一次、写一次(赶在绘制前):

4 · 想做 X,该用哪个?

你想… 因为
同步代码后、渲染前插入处理 (批量 DOM、读最新状态) queueMicrotask / Promise.then 微任务紧贴当前任务尾,早于渲染
把工作推迟到「稍后」,让出主线程 setTimeout / MessageChannel 宏任务,本轮微任务+渲染之后
做动画、改样式不撕裂 requestAnimationFrame 绘制前执行,对齐刷新率
节流高频事件 (scroll/resize) requestAnimationFrame 每帧最多一次,避免无用功
不紧急的后台任务 (预取、日志) requestIdleCallback / postTask(background) 绘制后趁空闲执行,不抢占交互
长时间计算不冻结 UI 切片 + scheduler.yield / MessageChannel 块间让出,保住输入响应
多个任务需分优先级 / 可取消 scheduler.postTask 三档优先级 + AbortController

一根主线,贯穿全系列:主线程只有一个,事件循环按「宏任务 → 微任务清空 → 渲染 (rAF) → 空闲 (rIC)」转圈。所有关于卡顿与响应是否及时的问题,本质都是你把代码放在了这一圈的哪个时机是否按时让出主线程。现代指标 INP (Interaction to Next Paint) 量的就是这件事:从用户交互到下一次绘制的延迟——别让长任务堵在中间。