React 的并发渲染能力依赖一个精巧的调度系统——Scheduler。它的核心职责是管理不同优先级的任务,并在浏览器空闲时执行可中断的渲染工作,确保用户交互始终流畅。
为什么需要任务调度
浏览器主线程的任何长时间任务(超过 50ms)都会导致掉帧。在传统同步渲染模式下,一旦开始渲染一棵大型组件树,就必须一口气完成,这将阻塞用户输入和动画,造成“卡顿感”。
任务调度器的目标就是将连续的大块渲染任务拆分成多个小任务,并利用浏览器空闲时段逐步执行。如果期间有更高优先级的用户交互发生,调度器会暂停当前低优先级任务,把主线程交还给浏览器,优先处理输入,等空闲时再恢复渲染。
浏览器空闲时间与 requestIdleCallback
浏览器提供了 requestIdleCallback API,允许我们在每一帧(通常 16.6ms)内浏览器完成关键工作后,剩余的空闲时间中执行任务。
一帧时间线:
|-- 输入事件 --|-- 定时器 --|-- 开始帧 --|-- 样式计算/布局/绘制 --|-- 空闲期 --|
调用 requestIdleCallback(callback) 后,浏览器会在空闲时段执行 callback,并传入一个 IdleDeadline 对象,其中 timeRemaining() 方法返回当前空闲期还剩多少毫秒,可以用来判断是否还有时间执行更多任务。
requestIdleCallback((deadline) => {
while (deadline.timeRemaining() > 0 && taskQueue.length > 0) {
const task = taskQueue.shift();
task(); // 执行任务
}
});
React Scheduler 的实现:为什么不用 requestIdleCallback
虽然概念相似,但 React Scheduler 并未直接使用 requestIdleCallback,而是基于 MessageChannel + 优先级队列 实现了自己的调度器。主要原因:
- 更可靠的调度频率:
requestIdleCallback在一些浏览器中触发频率较低(每 50ms 一次),这会导致 React 的并发渲染不够精细,无法适应 120Hz 高刷屏幕。 - 优先级控制:React 需要更灵活的任务优先级(例如用户输入 > 动画 > 数据拉取),
requestIdleCallback只有简单的空闲时执行,无法区分紧急程度。 - 跨环境一致性:React 的 scheduler 包不仅用于浏览器,也用于 React Native 等环境,需要统一的调度机制。
Scheduler 使用 MessageChannel 发送宏任务消息,在每个宏任务执行的间隙,去检查任务队列并决定是否执行。它的内部维护一个基于过期时间的优先级队列(Lane 模型映射为任务优先级),并根据当前时间以及剩余帧预算决定是否继续执行。
时间切片(Time Slicing)原理
时间切片是调度系统落地的关键。其工作流程:
- 当一次更新被触发,React 创建一个渲染任务,附上优先级和过期时间。
- Scheduler 将该任务放入队列,并请求一个宏任务回调。
- 宏任务执行时,React 进入 Render 阶段(可中断),从 Fiber 根开始遍历,每完成一个单元(一个 Fiber 节点的处理)就检查一次当前时间。
- 如果执行时间已经超出帧预算(例如 5ms),React 会暂停遍历,将剩余工作交还给调度器,由调度器在下一个空闲时机继续。
- 如果期间有更高优先级的任务(例如用户点击)被推入,Scheduler 会中断当前低优先级渲染,转而执行高优先级更新,完成后再恢复原先的渲染。
这种机制保证即使是深更新也不会长时间阻塞主线程,显著提升了应用在交互时的响应速度。
实际表现与调试
在 React DevTools 的 Profiler 中,可以观察到每个 Fiber 的渲染耗时,以及任务切片之间的断开。更直观地,使用浏览器的 Performance 面板纪录一段操作,能看到 React 的渲染任务分散在多个帧中,其间穿插着用户输入处理。
调度器的可扩展性
React 19 进一步暴露了调度相关的能力,例如 useTransition 和 useDeferredValue 就是调度器的高层抽象。开发者无需直接操作任务队列,只需标记哪些更新是“非紧急”的,React 会自动将其分配为低优先级,交给调度器去利用空闲时间处理。
简而言之,React 的调度系统巧妙地将浏览器空闲时间利用与现代优先级调度结合起来,让大型应用的渲染像流水线一样可中断、可恢复,最终实现流畅的用户体验。