Scheduler:React 的并发调度系统
第26章:Scheduler:React 的并发调度系统
React 的并发模式能中断低优先级工作,根源不在 Fiber 本身,而在其上的调度层——Scheduler。
本章核心问题:React 为什么需要自己的调度器?五个优先级级别如何映射到 Lanes? 读完本章你将理解:
- 浏览器原生 rAF 和 rIC 都不满足 React 的调度需求
- Scheduler 使用 MessageChannel 创建宏任务实现时间切片,避免 setTimeout 的 4ms 最小延迟
- Lanes 使用位掩码表示优先级,支持高效的合并、检查和移除操作
Level 1 · 你需要知道的(1-3年经验)
为什么 React 需要自己的调度器
在实现 Scheduler 之前,React 团队考察了浏览器原生提供的两个调度 API:
requestAnimationFrame(rAF):rAF 回调在每帧绘制前执行,理论上可以利用它来做渲染调度。但 rAF 的问题是:它的执行时机完全由浏览器决定,不能保证回调有足够的执行时间;而且当标签页不可见时,rAF 会停止调用,这意味着后台数据处理也会停止,不符合 React 的需求。
requestIdleCallback(rIC):rIC 在主线程空闲时执行,看起来完美匹配"利用空闲时间做 React 渲染"的需求。但 rIC 有两个致命问题:首先,它的浏览器支持不完整(Safari 直到最近才支持);其次,它的调用频率太低——rIC 的最小超时时间是 1 秒,对于流畅的交互来说太长了。
React 团队最终选择自己实现调度器。核心思路是:利用 MessageChannel 创建宏任务,在每个宏任务中执行一段时间的工作,然后让出控制权给浏览器处理事件和渲染,再通过下一个宏任务继续工作。
五个优先级级别
Scheduler 定义了五个优先级常量,每个优先级对应不同的过期时间(超过过期时间的任务会被强制执行,不再等待空闲时机):
// packages/scheduler/src/SchedulerPriorities.js
export const NoPriority = 0;
export const ImmediatePriority = 1; // 同步优先级,过期时间:-1(立即过期)
export const UserBlockingPriority = 2; // 用户交互优先级,过期时间:250ms
export const NormalPriority = 3; // 普通优先级,过期时间:5000ms
export const LowPriority = 4; // 低优先级,过期时间:10000ms
export const IdlePriority = 5; // 空闲优先级,过期时间:永不过期
// packages/scheduler/src/SchedulerFeatureFlags.js
const IMMEDIATE_PRIORITY_TIMEOUT = -1;
const USER_BLOCKING_PRIORITY_TIMEOUT = 250;
const NORMAL_PRIORITY_TIMEOUT = 5000;
const LOW_PRIORITY_TIMEOUT = 10000;
const IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt; // 永不过期
这些优先级映射到 React 层面的 Lanes 模型,进而影响哪些更新先执行:
ImmediatePriority→SyncLane(同步模式,不进入调度器,直接执行)UserBlockingPriority→InputContinuousLane(用户持续输入,如拖拽)NormalPriority→DefaultLane(普通 setState)LowPriority/IdlePriority→TransitionLane/OffscreenLane
workLoop:调度器的核心循环
// packages/scheduler/src/forks/Scheduler.js(简化)
function workLoop(hasTimeRemaining, initialTime) {
let currentTime = initialTime;
// 将 timerQueue 中已到执行时间的任务移入 taskQueue
advanceTimers(currentTime);
currentTask = peek(taskQueue);
while (currentTask !== null && !enableSchedulerDebugging) {
if (currentTask.expirationTime > currentTime && (!hasTimeRemaining || shouldYieldToHost())) {
// 当前任务未过期,且时间片已用完,让出控制权
break;
}
const callback = currentTask.callback;
if (typeof callback === 'function') {
currentTask.callback = null;
currentPriorityLevel = currentTask.priorityLevel;
// 检查任务是否过期
const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
// 执行任务,传入是否超时的标志
const continuationCallback = callback(didUserCallbackTimeout);
currentTime = getCurrentTime();
if (typeof continuationCallback === 'function') {
// 任务没执行完,返回了一个延续函数
// 将延续函数设置为同一任务的新 callback
currentTask.callback = continuationCallback;
} else {
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
}
advanceTimers(currentTime);
} else {
pop(taskQueue);
}
currentTask = peek(taskQueue);
}
if (currentTask !== null) {
return true; // 还有未完成的任务
} else {
// taskQueue 空了,检查 timerQueue
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
return false;
}
}
Level 2 · 它是怎么运行的(3-5年经验)
小顶堆:任务队列的实现
Scheduler 内部维护两个小顶堆(Min Heap):
taskQueue:已过期或当前应该执行的任务队列timerQueue:尚未到执行时间的延迟任务队列
小顶堆可以在 O(log n) 时间内完成任务的插入和取出最小值操作,非常适合优先级调度场景:
// packages/scheduler/src/SchedulerMinHeap.js
export function push(heap, node) {
const index = heap.length;
heap.push(node);
siftUp(heap, node, index);
}
export function peek(heap) {
return heap.length === 0 ? null : heap[0];
}
export function pop(heap) {
if (heap.length === 0) return null;
const first = heap[0];
const last = heap.pop();
if (last !== first) {
heap[0] = last;
siftDown(heap, last, 0);
}
return first;
}
function siftUp(heap, node, i) {
while (i > 0) {
const parentIndex = (i - 1) >>> 1; // 右移1位相当于除以2取整
const parent = heap[parentIndex];
if (compare(parent, node) > 0) {
// 父节点优先级低于当前节点,上移
heap[parentIndex] = node;
heap[i] = parent;
i = parentIndex;
} else {
return;
}
}
}
function compare(a, b) {
// 优先按 sortIndex(过期时间)排序,相同时按 id(创建顺序)排序
const diff = a.sortIndex - b.sortIndex;
return diff !== 0 ? diff : a.id - b.id;
}
任务对象的结构如下:
// scheduleCallback 创建的任务对象
var newTask = {
id: taskIdCounter++,
callback, // 实际要执行的工作函数
priorityLevel,
startTime, // 任务开始时间
expirationTime, // 过期时间(startTime + priorityTimeout)
sortIndex: -1, // 在堆中的排序键
};
MessageChannel:时间切片的核心机制
Scheduler 使用 MessageChannel 来创建宏任务,实现时间切片:
// packages/scheduler/src/forks/Scheduler.js(简化)
const channel = new MessageChannel();
const port = channel.port2;
// 每次发送消息,就会在下一个宏任务中执行 performWorkUntilDeadline
channel.port1.onmessage = performWorkUntilDeadline;
const schedulePerformWorkUntilDeadline = () => {
port.postMessage(null);
};
为什么不用 setTimeout(fn, 0)?原因很关键:HTML 规范规定,嵌套超过 5 层的 setTimeout 最小延迟为 4ms。这 4ms 的强制延迟对于 60fps 的每帧 16.6ms 来说是巨大的浪费。而 MessageChannel 没有这个限制,可以实现真正的零延迟调度。
// packages/scheduler/src/forks/Scheduler.js
function performWorkUntilDeadline() {
if (scheduledHostCallback !== null) {
const currentTime = getCurrentTime();
// 计算本次宏任务的截止时间
// frameInterval 默认是 5ms(每帧的工作时间预算)
deadline = currentTime + frameInterval;
const hasMoreWork = scheduledHostCallback(true, currentTime);
if (!hasMoreWork) {
isMessageLoopRunning = false;
scheduledHostCallback = null;
} else {
// 还有工作,继续发送消息触发下一个宏任务
port.postMessage(null);
}
} else {
isMessageLoopRunning = false;
}
}
Level 3 · 规范怎么定义的(资深)
shouldYieldToHost:让出控制权的决策
shouldYield(在内部叫 shouldYieldToHost)是并发模式的关键判断函数:
// packages/scheduler/src/forks/Scheduler.js
function shouldYieldToHost() {
const timeElapsed = getCurrentTime() - startTime;
if (timeElapsed < frameInterval) {
// 当前帧还有时间,继续执行
return false;
}
// 超过时间预算,让出控制权
// 注意:React 19 中这里还会检查 isInputPending(如果浏览器支持)
if (enableIsInputPending) {
if (needsPaint) {
// 浏览器需要绘制,立即让出
return true;
}
if (timeElapsed < continuousInputInterval) {
// 时间不长,检查是否有输入事件待处理
if (isInputPending !== null) {
return isInputPending();
}
} else if (timeElapsed < maxInterval) {
if (isInputPending !== null) {
return isInputPending(continuousOptions);
}
} else {
return true;
}
}
return true;
}
isInputPending 是 Chrome 提供的实验性 API(navigator.scheduling.isInputPending()),允许 JavaScript 在不让出控制权的情况下查询是否有未处理的用户输入事件。当没有输入事件时,React 可以在一个宏任务中执行更多工作,进一步减少调度开销。
Lanes 模型:React 18/19 的优先级细化
React 18 引入了 Lanes 模型作为比 Scheduler 优先级更细粒度的内部优先级系统:
// packages/react-reconciler/src/ReactFiberLane.js(部分)
export const NoLanes = /* */ 0b0000000000000000000000000000000;
export const SyncLane = /* */ 0b0000000000000000000000000000001;
export const InputContinuousHydrationLane = /* */ 0b0000000000000000000000000000010;
export const InputContinuousLane = /* */ 0b0000000000000000000000000000100;
export const DefaultHydrationLane = /* */ 0b0000000000000000000000000001000;
export const DefaultLane = /* */ 0b0000000000000000000000000010000;
export const TransitionHydrationLane = /* */ 0b0000000000000000000000000100000;
export const TransitionLanes = /* */ 0b0000000001111111111111111000000;
// ... 更多 lanes
Lanes 使用位掩码(bitmask)表示,这使得"合并多个优先级"、"检查某优先级是否在集合中"等操作可以用位运算高效完成:
// 合并两个 lane 集合
function mergeLanes(a, b) {
return a | b;
}
// 检查 lanes 是否包含某个 subset
function isSubsetOfLanes(set, subset) {
return (set & subset) === subset;
}
// 从集合中移除某些 lanes
function removeLanes(set, subset) {
return set & ~subset;
}
React 19 在 Lanes 模型上进一步扩展,为 Actions(异步 transitions)引入了专用的 lanes,确保 useOptimistic 更新和服务器响应能够按正确的优先级顺序处理。
Scheduler 和 Lanes 共同构成了 React 并发的底层基础设施。前者管理"什么时候执行工作",后者管理"哪些工作应该优先执行"。掌握这两个系统,你就真正理解了 React 并发不是魔法,而是精心设计的工程实现。
Level 4 · 边界与陷阱(所有人)
生产环境常见问题
在实际项目中,本章涵盖的概念最常见的问题包括:
-
忽视边界条件:在正常路径下工作正常的代码,在异常路径(网络失败、用户快速操作、组件卸载)下可能产生 bug。始终考虑清理和取消逻辑。
-
过早优化:在没有测量的情况下添加优化代码,增加了复杂度但不一定提升性能。先用 Profiler 确认问题存在,再实施优化。
-
文档与实际行为的差异:React 的行为在不同版本间可能有微妙差异,尤其是并发模式相关的特性。始终以实际测试结果为准。