对应源码:
Scheduler.js,SchedulerPriorities.js,SchedulerFeatureFlags.js
1. Scheduler 的角色
Scheduler 是 React 中独立于 Reconciler 的模块。它的职责是决定什么时候执行任务,按什么顺序执行。
没有 Scheduler:
setState → 立刻同步渲染 → 如果组件树很大,主线程被阻塞
有 Scheduler:
setState → 创建任务放入队列 → Scheduler 决定何时执行
→ 时间片到了才执行,时间片用完就暂停
→ 高优先级任务先执行
→ 主线程不被长时间独占2. 核心设计:合作式调度
Scheduler 实现的是合作式调度(Cooperative Scheduling)——任务主动检查是否应该让出主线程。React Design Principles 中的 Scheduling 章节解释了这一设计决策:
// ReactFiberWorkLoop.js
function workLoopConcurrentByScheduler() {
while (workInProgress !== null && !shouldYield()) {
// ^^^^^^^^^^
// 每个 Fiber 处理完后检查:还要不要继续?
performUnitOfWork(workInProgress);
}
}shouldYield() 检查当前时间片是否用完:
// packages/scheduler/src/forks/Scheduler.js
let frameInterval = frameYieldMs; // 默认 5ms
let startTime = -1;
let needsPaint = false; // 浏览器是否需要绘制
function shouldYieldToHost(): boolean {
if (!enableAlwaysYieldScheduler && enableRequestPaint && needsPaint) {
// 浏览器需要绘制了 → 立刻让出(优先于时间片检查)
return true;
}
const timeElapsed = getCurrentTime() - startTime;
if (timeElapsed < frameInterval) {
// 主线程被阻塞的时间还没超过帧间隔 → 继续
return false;
}
// 主线程被阻塞超过 frameInterval(5ms)→ 让出
return true;
}
function requestPaint() {
// React Commit 完成后调用,告诉 Scheduler:让浏览器有机会绘制
if (enableRequestPaint) {
needsPaint = true;
}
}needsPaint:与浏览器渲染的协调
needsPaint 是 Scheduler 和浏览器渲染之间的信号灯。理解它需要知道一个前提:Scheduler 在 MessageChannel 宏任务中执行 React 工作,这段时间浏览器无法绘制。
时间轴:
宏任务(React工作) 宏任务(浏览器渲染) 宏任务(下一轮React工作)
──────────────┐ ──────────────┐ ──────────────
shouldYield? │ 浏览器画一帧 │ needsPaint已被重置
needsPaint= │ 60fps动画 │ = false
true → 让出 │ 用户交互反馈 │
▼ ▼完整流程:
- React Commit 完成后调用
requestPaint()(ReactFiberWorkLoop.js:4189)→needsPaint = true - 如果 React 还有残余工作(下一轮
shouldYieldToHost()检查),needsPaint === true→ 立即让出,即使 5ms 时间片没用完 MessageChannel让出后,浏览器有机会执行渲染:画一帧、处理用户输入- 浏览器渲染完毕,下一轮
performWorkUntilDeadline开始 →needsPaint = false(重置) - React 继续处理残余工作
这个机制确保了:即使 React 有大量渲染工作,每次 Commit 后浏览器都能至少画一帧。注释明确写道"Tell Scheduler to yield at the end of the frame, so the browser has an opportunity to paint"。
enableAlwaysYieldScheduler(实验性 flag,__EXPERIMENTAL__)如果开启,会跳过needsPaint检查并在flushWork中对每个任务都 yield——用于调试"每帧只处理一个任务"的极端场景。
3. 五级优先级
// [`SchedulerPriorities.js`](https://github.com/facebook/react/blob/eafeac097b/packages/scheduler/src/SchedulerPriorities.js)
export const NoPriority = 0;
export const ImmediatePriority = 1; // 已弃用(见下方注释)
export const UserBlockingPriority = 2; // 用户交互
export const NormalPriority = 3; // 普通
export const LowPriority = 4; // 低
export const IdlePriority = 5; // 空闲ImmediatePriority 不再被 React 使用——
ReactFiberRootScheduler.js:482-484注释: "Scheduler does have an 'ImmediatePriority', but now that we use microtasks for sync work we no longer use that." Discrete 和 Continuous 事件在 Scheduler 层都映射为UserBlockingPriority(2)。
每个优先级对应的超时时间:
// [`SchedulerFeatureFlags.js`](https://github.com/facebook/react/blob/eafeac097b/packages/scheduler/src/SchedulerFeatureFlags.js)
export const frameYieldMs = 5; // 时间片:5ms
export const userBlockingPriorityTimeout = 250; // UserBlocking: 250ms 后过期
export const normalPriorityTimeout = 5000; // Normal: 5000ms 后过期
export const lowPriorityTimeout = 10000; // Low: 10000ms 后过期超时意味着"如果这个时间后任务还没执行,就不管优先级了,必须执行"。
4. 时间切片的实现:MessageChannel
Scheduler 不用 requestIdleCallback(兼容性差、行为不稳定),而是用 MessageChannel。React 技术揭秘 对这一选择的原因做了分析:
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
function schedulePerformWorkUntilDeadline() {
port.postMessage(null);
// postMessage 是宏任务,在下一个事件循环执行
// 浏览器有机会在这之前做其他事(如处理用户输入、渲染)
}
function performWorkUntilDeadline() {
if (isMessageLoopRunning) {
const currentTime = getCurrentTime();
// 记录开始时间,用于测量主线程被阻塞多久
startTime = currentTime;
let hasMoreWork = true;
try {
hasMoreWork = flushWork(currentTime);
} finally {
if (hasMoreWork) {
// 还有工作 → 再次调度
schedulePerformWorkUntilDeadline();
} else {
isMessageLoopRunning = false;
}
}
}
}时间切片的工作方式:
[Scheduler 开始工作]
startTime = now()
执行任务...
↓ shouldYield() → now() - startTime < 5ms → 继续
执行任务...
↓ shouldYield() → now() - startTime < 5ms → 继续
执行任务...
↓ shouldYield() → now() - startTime >= 5ms → true → 让出
[让出主线程]
浏览器处理渲染、用户输入...
↓ 下一个事件循环
MessageChannel.onmessage → 继续执行
循环直到所有任务完成5. 任务调度的入口
function scheduleCallback(priorityLevel, callback) {
const currentTime = getCurrentTime();
const startTime = currentTime;
let timeout;
switch (priorityLevel) {
case ImmediatePriority:
timeout = -1; // 立即过期
break;
case UserBlockingPriority:
timeout = userBlockingPriorityTimeout; // 250ms
break;
case NormalPriority:
timeout = normalPriorityTimeout; // 5000ms
break;
case LowPriority:
timeout = lowPriorityTimeout; // 10000ms
break;
case IdlePriority:
timeout = maxSigned31BitInt; // 永不过期(直到空闲)
break;
}
const expirationTime = startTime + timeout;
const newTask = {
id: taskIdCounter++,
callback, // 要执行的函数
priorityLevel,
startTime, // 开始时间
expirationTime, // 过期时间(用于排序)
sortIndex: -1, // 排序索引(初始设为 expirationTime)
};
if (startTime > currentTime) {
// 延迟任务 → 放入 timerQueue
newTask.sortIndex = startTime;
push(timerQueue, newTask);
} else {
// 立即任务 → 放入 taskQueue
newTask.sortIndex = expirationTime;
push(taskQueue, newTask);
// 如果没有正在执行的任务,开始调度
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);
}
}
return newTask;
}6. flushWork:取出任务执行
function flushWork(initialTime) {
isHostCallbackScheduled = false;
isPerformingWork = true;
let currentTime = initialTime;
let currentTask = peek(taskQueue); // 从堆顶取最高优先级任务
while (currentTask !== null) {
if (currentTask.callback === null) {
// 任务被取消了
pop(taskQueue);
} else if (currentTask.expirationTime > currentTime && shouldYieldToHost()) {
// 任务未过期 且 时间片用完 → 暂停
break;
} else {
// 执行任务
const continuationCallback = currentTask.callback(currentTime);
if (typeof continuationCallback === 'function') {
// 任务返回了续接函数 → 更新任务,放回队列
currentTask.callback = continuationCallback;
} else {
// 任务完成 → 弹出
pop(taskQueue);
}
currentTask = peek(taskQueue); // 取下一个
currentTime = getCurrentTime();
}
}
isPerformingWork = false;
return currentTask !== null; // 是否还有任务
}