Skip to content
目录

Scheduler 调度器

1. Scheduler 是什么

Scheduler 是 React monorepo 里独立包packages/scheduler/),负责在浏览器主线程上安排「何时执行哪段 JS」。Reconciler(Fiber)决定要渲染什么、优先级多高(Lanes);Scheduler 决定这段渲染工作何时跑、跑多久、要不要先让给输入和绘制

可以把它理解成 React 并发模式下的「操作系统调度器」:不是替代事件循环,而是在 MessageChannel / setTimeout 等宿主 API 之上,再包一层可中断的任务队列

1.1 解决什么问题

问题Scheduler 的做法
大组件树一次 render 卡死主线程时间切片:每帧只干一小段,通过 shouldYieldToHost 主动让出
点击、输入被慢更新挡住优先级 + 过期时间:紧急任务 sooner 到期,先被 workLoop 执行
同一时刻多个更新最小堆队列:按 expirationTime 取最急的任务
渲染没做完又要更新continuation:回调可返回函数,表示「下次接着干」

1.2 在整体架构中的位置

mermaid
flowchart TB
  subgraph react [React Reconciler]
    U[setState / 事件] --> L[requestUpdateLane / Lanes]
    L --> E[ensureRootIsScheduled]
    E --> M[lanesToEventPriority]
  end
  subgraph sched [Scheduler 包]
    M --> SC[scheduleCallback]
    SC --> Q[taskQueue 最小堆]
    Q --> WL[workLoop + shouldYield]
  end
  subgraph host [浏览器]
    WL --> MC[MessageChannel / 宿主回调]
    MC --> Paint[布局 / 绘制 / 输入]
  end

更细的 Lane 分档见 02-Fiber架构详解;Hook 侧如何打上 lane 见 04-Hooks实现机制


2. 三层优先级:别和 Fiber 的 Lanes 混为一谈

React 里常出现三套「优先级」名称,层级不同:

层级在哪干什么
LanesReactFiberLane.js标记这次更新有多急(Sync、Default、Transition…)
Event PriorityReconciler 内部把 Lane 粗分为 Discrete / Continuous / Default / Idle 等事件语义
Scheduler PrioritySchedulerPriorities.js决定调度任务进哪个堆、多久必须跑完

Reconciler 在 ensureRootIsScheduled 里会根据 pending lanes 选出最高档 lane,再映射到 Scheduler 的 scheduleCallback(priorityLevel, performConcurrentWorkOnRoot)Lanes 描述「更新」;Scheduler 描述「执行任务」

2.1 Scheduler 的五档优先级

定义在 packages/scheduler/src/SchedulerPriorities.js(数值越小越急):

常量含义典型来源(经 Reconciler 映射后)
ImmediatePriority (1)立刻、同步语义SyncLaneflushSync
UserBlockingPriority (2)用户可感知阻塞连续输入、部分高优交互
NormalPriority (3)默认普通 setState、Transition 常落在此档
LowPriority (4)可延后较低优后台更新
IdlePriority (5)空闲才做IdleLanes、低优副作用

2.2 过期时间(expirationTime)才是堆里的「闹钟」

调度时不直接比较「优先级数字」,而是为每个任务算 expirationTime = startTime + timeout,堆顶取最早到期的任务。不同档位的默认 timeout(源码常量,单位 ms):

优先级timeout 策略直观理解
Immediate-1(已过期)本轮就要跑
UserBlocking250约四分之一秒内必须处理,否则算「饿死」要提权
Normal5000普通工作默认 5s 内要轮到
Low10000更宽松
Idle接近 31 位整数上限几乎不因过期而抢跑,真闲了才执行

小结:优先级决定「多快变成过期」;过期后 workLoop 会强制执行该任务,即使时间片本该让出——这是「饥饿」防护,而不是严格的抢占式 OS 调度。


3. 任务队列:两个堆,一种任务对象

3.1 taskQueue 与 timerQueue

  • taskQueuestartTime <= now 的可执行队列,按 expirationTime 入最小堆(SchedulerMinHeap.js)。
  • timerQueue:带 delay 的延迟任务,先按 startTime 排队;时间到了再挪进 taskQueue

这样「5 秒后再跑」不会挡住「250ms 内必须跑完」的交互任务。

3.2 任务长什么样(概念字段)

每个 Task 大致包含:callbackpriorityLevelstartTimeexpirationTimesortIndex(堆排序键)。scheduleCallback 创建任务后,若当前没有在跑的工作,会通过 requestHostCallback 向浏览器预约一次 flushWork

3.3 continuation:渲染可以「干一半先停」

workLoop 执行 callback 时,若返回值是函数,表示任务未完成,Scheduler 把该函数设回 task.callback,下次时间片继续。Fiber 的 performConcurrentWorkOnRoot 正是利用这一点:一棵大树可以多帧才遍历完,中间穿插输入和绘制。


4. 时间切片与 shouldYieldToHost

4.1 原理

默认 frameInterval ≈ 5ms:单次 flushWork 里,从开始执行到 shouldYieldToHost() 为 true,大约只占用几毫秒,然后交还主线程。下一帧(或下一个宏任务)再继续堆顶任务。

这不是固定 60fps 一帧,而是软时间片;真正的帧率仍由浏览器与 requestAnimationFrame 决定。

4.2 何时让出(总结逻辑)

shouldYieldToHost 在「时间片用尽」基础上,还会因宿主信号提前让出:

条件原因
未超过 frameInterval继续执行当前切片
needsPaint需要先绘制,避免饿死视觉更新
isInputPending()(支持的浏览器)检测到有连续输入在等,优先响应用户
超过 maxInterval 等上限防止单任务占用过长

因此并发渲染不是「算得慢就丢工作」,而是可推迟、不可永久霸占主线程

mermaid
sequenceDiagram
  participant H as 浏览器
  participant F as flushWork
  participant W as workLoop
  participant C as Fiber callback
  H->>F: MessageChannel 触发
  F->>W: 循环取堆顶任务
  W->>C: 执行 performWork
  alt shouldYield 或 未到期
    W-->>H: 让出主线程
    H->>F: 下一宏任务继续
  else 任务返回 continuation
    W->>W: 保留 callback 下次续跑
  end

5. 核心流程(无长源码版)

5.1 scheduleCallback:入队

  1. 根据 priorityLeveltimeout,算出 expirationTime
  2. 若有 options.delay,进 timerQueue 并预约 requestHostTimeout;否则进 taskQueue
  3. 若当前没有正在执行的 work,requestHostCallback(flushWork) 挂到宿主。

对外 API 常写作 unstable_scheduleCallback(历史命名,React 内部稳定使用)。

5.2 flushWork → workLoop:出队执行

  1. flushWork 标记 isPerformingWork,保存/恢复当前优先级上下文。
  2. workLoop 不断 peek 堆顶:若任务未过期应该 yield,则 break,返回「还有活」。
  3. 执行 callback(didUserCallbackTimeout)didUserCallbackTimeout 为 true 表示已错过 expiration,Reconciler 可据此决定是否同步收尾。
  4. 堆空则返回 false;宿主可不再预约回调。若 timerQueue 还有未来任务,用 requestHostTimeout 唤醒。

5.3 cancelCallback

取消任务并不从堆里立刻删除节点,而是把 task.callback = null;轮到该任务时发现无回调则弹出。简单、避免堆中间删除的复杂度。


6. 与 React Reconciler 的集成

6.1 谁调用 Scheduler

ReactFiberWorkLoop.jsensureRootIsScheduled 中:根据 root 上 pending 的 lanes 得到 nextLanes,再 getHighestPriorityLanelanesToEventPriority → 映射到 Scheduler 的 Immediate / UserBlocking / Normal / Idle 等,最后 scheduleCallback(priority, () => performConcurrentWorkOnRoot(root))

SyncLane 且满足条件时可能走 flushSync 路径,不经过 Scheduler 切片,直接在调用栈上刷完(Legacy 兼容与强一致场景)。

6.2 和 Hooks、批处理的关系

  • 多次 setState 合并成 root 上一次调度;Scheduler 只看到一个(或少量)渲染任务。
  • React 18 自动批处理 发生在 Reconciler;Scheduler 不负责批处理,只负责已决定渲染的那次 work 如何切开执行
  • useTransition 把更新标到 TransitionLanes,映射后往往是 Normal/Low,从而不挡住 Sync / UserBlocking。

6.3 与 Fiber「可中断」的边界

  • Render 阶段beginWork / completeWork)可在时间片之间暂停;Commit 阶段同步、不可中断。
  • Scheduler 切片的是 Reconciler 交给它的 callback;Commit、部分 useLayoutEffect 不在「可随便 yield」的语义里。

7. 宿主调度与性能要点

7.1 宿主 API

浏览器端常用 MessageChannel(或 setImmediate polyfill)触发 flushWork,比单纯 requestIdleCallback 更可预测。Scheduler 与具体宿主解耦,React Native 等可换 fork。

7.2 设计带来的体验

机制用户侧感受
时间切片大列表更新时输入仍跟手
过期提权低优任务不会永远饿死
continuation长渲染跨帧完成,不一次卡死
高优插队新点击可打断未完成的 Transition 渲染

7.3 不必过度依赖的细节

  • 精确毫秒数可能随版本调整,读源码以 Scheduler.js 里常量为准。
  • 生产环境很少直接 import 'scheduler';理解模型即可,自定义调度一般用 startTransition / useDeferredValue 等公开 API。

8. 源码阅读路径

顺序文件关注什么
1SchedulerPriorities.js五档优先级
2forks/Scheduler.jsscheduleCallbackworkLoopshouldYieldToHost、timeout 常量
3SchedulerMinHeap.js堆操作
4ReactFiberWorkLoop.jsensureRootIsScheduledlanesToEventPriorityscheduleCallback 调用点

9. 总结

  1. Scheduler 是独立调度层:管理主线程上的 JS 任务队列,不是 Fiber 本身。
  2. 五档优先级 + expirationTime:越急的任务越早「到期」,堆顶先执行。
  3. 双队列 + 时间切片 + continuation:支持延迟、可中断、可续跑的长时间 render。
  4. 与 Lanes 配合:Reconciler 决定更新急缓,Scheduler 决定 work 何时跑、跑多久;二者通过 scheduleCallback 衔接。
  5. Commit 不可切片:并发体验来自 Render 阶段让出,而非无限推迟 DOM 提交。

结合 02-Fiber架构详解 的 Lanes 与 04-Hooks实现机制dispatchSetState,可以把「点按钮 → lane → 调度 → 多帧 render → commit」串成一条完整链路。

参考

MIT License.