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 在整体架构中的位置
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 里常出现三套「优先级」名称,层级不同:
| 层级 | 在哪 | 干什么 |
|---|---|---|
| Lanes | ReactFiberLane.js | 标记这次更新有多急(Sync、Default、Transition…) |
| Event Priority | Reconciler 内部 | 把 Lane 粗分为 Discrete / Continuous / Default / Idle 等事件语义 |
| Scheduler Priority | SchedulerPriorities.js | 决定调度任务进哪个堆、多久必须跑完 |
Reconciler 在 ensureRootIsScheduled 里会根据 pending lanes 选出最高档 lane,再映射到 Scheduler 的 scheduleCallback(priorityLevel, performConcurrentWorkOnRoot)。Lanes 描述「更新」;Scheduler 描述「执行任务」。
2.1 Scheduler 的五档优先级
定义在 packages/scheduler/src/SchedulerPriorities.js(数值越小越急):
| 常量 | 含义 | 典型来源(经 Reconciler 映射后) |
|---|---|---|
ImmediatePriority (1) | 立刻、同步语义 | SyncLane、flushSync |
UserBlockingPriority (2) | 用户可感知阻塞 | 连续输入、部分高优交互 |
NormalPriority (3) | 默认 | 普通 setState、Transition 常落在此档 |
LowPriority (4) | 可延后 | 较低优后台更新 |
IdlePriority (5) | 空闲才做 | IdleLanes、低优副作用 |
2.2 过期时间(expirationTime)才是堆里的「闹钟」
调度时不直接比较「优先级数字」,而是为每个任务算 expirationTime = startTime + timeout,堆顶取最早到期的任务。不同档位的默认 timeout(源码常量,单位 ms):
| 优先级 | timeout 策略 | 直观理解 |
|---|---|---|
| Immediate | -1(已过期) | 本轮就要跑 |
| UserBlocking | 250 | 约四分之一秒内必须处理,否则算「饿死」要提权 |
| Normal | 5000 | 普通工作默认 5s 内要轮到 |
| Low | 10000 | 更宽松 |
| Idle | 接近 31 位整数上限 | 几乎不因过期而抢跑,真闲了才执行 |
小结:优先级决定「多快变成过期」;过期后 workLoop 会强制执行该任务,即使时间片本该让出——这是「饥饿」防护,而不是严格的抢占式 OS 调度。
3. 任务队列:两个堆,一种任务对象
3.1 taskQueue 与 timerQueue
- taskQueue:
startTime <= now的可执行队列,按expirationTime入最小堆(SchedulerMinHeap.js)。 - timerQueue:带
delay的延迟任务,先按startTime排队;时间到了再挪进taskQueue。
这样「5 秒后再跑」不会挡住「250ms 内必须跑完」的交互任务。
3.2 任务长什么样(概念字段)
每个 Task 大致包含:callback、priorityLevel、startTime、expirationTime、sortIndex(堆排序键)。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 等上限 | 防止单任务占用过长 |
因此并发渲染不是「算得慢就丢工作」,而是可推迟、不可永久霸占主线程。
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 下次续跑
end5. 核心流程(无长源码版)
5.1 scheduleCallback:入队
- 根据
priorityLevel选timeout,算出expirationTime。 - 若有
options.delay,进 timerQueue 并预约requestHostTimeout;否则进 taskQueue。 - 若当前没有正在执行的 work,
requestHostCallback(flushWork)挂到宿主。
对外 API 常写作 unstable_scheduleCallback(历史命名,React 内部稳定使用)。
5.2 flushWork → workLoop:出队执行
flushWork标记isPerformingWork,保存/恢复当前优先级上下文。workLoop不断peek堆顶:若任务未过期且应该 yield,则 break,返回「还有活」。- 执行
callback(didUserCallbackTimeout):didUserCallbackTimeout为 true 表示已错过 expiration,Reconciler 可据此决定是否同步收尾。 - 堆空则返回 false;宿主可不再预约回调。若 timerQueue 还有未来任务,用
requestHostTimeout唤醒。
5.3 cancelCallback
取消任务并不从堆里立刻删除节点,而是把 task.callback = null;轮到该任务时发现无回调则弹出。简单、避免堆中间删除的复杂度。
6. 与 React Reconciler 的集成
6.1 谁调用 Scheduler
ReactFiberWorkLoop.js 在 ensureRootIsScheduled 中:根据 root 上 pending 的 lanes 得到 nextLanes,再 getHighestPriorityLane → lanesToEventPriority → 映射到 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. 源码阅读路径
| 顺序 | 文件 | 关注什么 |
|---|---|---|
| 1 | SchedulerPriorities.js | 五档优先级 |
| 2 | forks/Scheduler.js | scheduleCallback、workLoop、shouldYieldToHost、timeout 常量 |
| 3 | SchedulerMinHeap.js | 堆操作 |
| 4 | ReactFiberWorkLoop.js | ensureRootIsScheduled、lanesToEventPriority、scheduleCallback 调用点 |
9. 总结
- Scheduler 是独立调度层:管理主线程上的 JS 任务队列,不是 Fiber 本身。
- 五档优先级 + expirationTime:越急的任务越早「到期」,堆顶先执行。
- 双队列 + 时间切片 + continuation:支持延迟、可中断、可续跑的长时间 render。
- 与 Lanes 配合:Reconciler 决定更新急缓,Scheduler 决定 work 何时跑、跑多久;二者通过
scheduleCallback衔接。 - Commit 不可切片:并发体验来自 Render 阶段让出,而非无限推迟 DOM 提交。
结合 02-Fiber架构详解 的 Lanes 与 04-Hooks实现机制 的 dispatchSetState,可以把「点按钮 → lane → 调度 → 多帧 render → commit」串成一条完整链路。