🧑💻 前言
在上一篇文章里,我们已经走到 scheduleUpdateOnFiber
,它会把更新注册到 Scheduler 调度器。
那么 React 是如何利用 Scheduler 来决定「哪个任务先执行、哪个可以延迟」,以及如何实现「时间切片 + 可中断渲染」的呢?
本篇我们就来源码级别地拆解 Scheduler 的实现。


1. 调度器整体架构
React Scheduler 大体分为三个部分:
js
Scheduler 架构
│
├─ 调度内核 (SchedulerHostConfig.js)
│ ├─ requestHostCallback - 请求宿主环境执行回调
│ ├─ cancelHostCallback - 取消回调
│ ├─ shouldYieldToHost - 是否应该让出主线程
│ └─ getCurrentTime - 获取当前时间
│
├─ 任务队列管理 (Scheduler.js)
│ ├─ taskQueue - 立即任务队列(最小堆)
│ ├─ timerQueue - 延迟任务队列(最小堆)
│ ├─ unstable_scheduleCallback - 任务调度入口
│ └─ workLoop - 消费任务的循环
│
└─ 优先级系统
├─ ImmediatePriority - 立即执行 (-1ms)
├─ UserBlockingPriority - 用户阻塞 (250ms)
├─ NormalPriority - 正常 (5000ms)
├─ LowPriority - 低优先级 (10000ms)
└─ IdlePriority - 空闲 (最大值,不会过期)
一句话总结:Scheduler = React 的任务大脑,负责优先级、队列和调度循环。
2. 调度内核实现
2.1 消息循环机制
调度器会通过 performWorkUntilDeadline
来消费任务,利用不同环境的「异步 API」来驱动循环。
js
// 核心调度函数
const performWorkUntilDeadline = () => {
if (scheduledHostCallback !== null) {
const currentTime = getCurrentTime();
// 设置时间切片截止时间
deadline = currentTime + yieldInterval; // 默认 5ms
const hasTimeRemaining = true;
try {
// 执行调度回调(flushWork),返回是否还有剩余工作
const hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
if (!hasMoreWork) {
// 没有剩余任务,关闭消息循环
isMessageLoopRunning = false;
scheduledHostCallback = null;
} else {
// 还有任务,继续调度下一次
port.postMessage(null);
}
} catch (error) {
// 出错也要继续调度,避免中断
port.postMessage(null);
throw error;
}
} else {
isMessageLoopRunning = false;
}
needsPaint = false;
};
解释:
scheduledHostCallback
→ 保存当前要执行的工作函数(一般是flushWork
)。deadline
→ 本次时间切片的结束时间,默认为 5ms。- 如果任务执行完了 → 退出循环;否则继续通过
MessageChannel
或其他方式调度下一次。

2.2 调度机制选择
Scheduler 会在不同环境下选择最优的消息调度方式:
js
let schedulePerformWorkUntilDeadline;
if (typeof localSetImmediate === 'function') {
// Node.js 环境,优先使用 setImmediate
schedulePerformWorkUntilDeadline = () => {
localSetImmediate(performWorkUntilDeadline);
};
} else if (typeof MessageChannel !== 'undefined') {
// 浏览器环境,避免 setTimeout 的 4ms 延迟
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
schedulePerformWorkUntilDeadline = () => {
port.postMessage(null);
};
} else {
// 兜底:setTimeout
schedulePerformWorkUntilDeadline = () => {
localSetTimeout(performWorkUntilDeadline, 0);
};
}
解释:
- Node.js →
setImmediate
(最快) - 浏览器 →
MessageChannel
(避免 4ms 延迟) - 兜底 →
setTimeout
这保证了 Scheduler 在不同运行环境下都能以「最小延迟」触发任务。
2.3 时间切片实现
每次调度循环都会判断时间片,判断是否过期,是否超过时间片,是否有高优先级任务,判断是否跳出本次循环
js
let yieldInterval = 5; // 默认每个切片 5ms
let deadline = 0;
const maxYieldInterval = 300;
let needsPaint = false;
// 判断是否需要让出主线程
shouldYieldToHost = function () {
const currentTime = getCurrentTime();
if (currentTime >= deadline) {
// 超过了本次时间切片的截止点
if (needsPaint || (navigator.scheduling && navigator.scheduling.isInputPending())) {
// 有待绘制或有用户输入 → 立即让出
return true;
}
// 即使没有用户操作,也要避免超过最大间隔
return currentTime >= maxYieldInterval;
} else {
// 还没到 5ms 截止点,可以继续执行
return false;
}
};
解释:
- 每次任务执行时都会检查是否超过 时间片截止点 (5ms) 。
- 如果超时 → 让出主线程给浏览器绘制或用户交互。
- 最长也不会超过 300ms,避免长时间霸占线程。
3. 任务创建和管理
3.1 任务创建流程
unstable_scheduleCallback
是外部调用的入口,用来创建一个任务。
js
function unstable_scheduleCallback(priorityLevel, callback, options) {
var currentTime = getCurrentTime();
// 计算任务开始时间
var startTime;
if (typeof options === 'object' && options !== null) {
var delay = options.delay;
startTime = (typeof delay === 'number' && delay > 0)
? currentTime + delay
: currentTime;
} else {
startTime = currentTime;
}
// 根据优先级确定过期时间
var timeout;
switch (priorityLevel) {
case ImmediatePriority: timeout = -1; break;
case UserBlockingPriority: timeout = 250; break;
case IdlePriority: timeout = IDLE_PRIORITY_TIMEOUT; break;
case LowPriority: timeout = 10000; break;
case NormalPriority:
default: timeout = 5000; break;
}
var expirationTime = startTime + timeout;
// 构建任务对象
var newTask = {
id: taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex: -1,
};
// 判断放入哪个队列
if (startTime > currentTime) {
// 延迟任务 → timerQueue
newTask.sortIndex = startTime;
push(timerQueue, newTask);
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
// 立即任务 → taskQueue
newTask.sortIndex = expirationTime;
push(taskQueue, newTask);
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);
}
}
return newTask;
}
解释:
-
计算 startTime → 是否有
delay
参数。 -
计算过期时间 expirationTime → 根据优先级超时策略。
-
构建任务对象 → 包含 id、callback、优先级、开始/过期时间。
-
入队:
- 延迟任务 →
timerQueue
- 立即任务 →
taskQueue
并触发调度 (requestHostCallback
)
- 延迟任务 →
3.2 任务对象结构
js
var newTask = {
id: taskIdCounter++, // 唯一任务 ID
callback, // 任务执行函数
priorityLevel, // 优先级
startTime, // 任务开始时间
expirationTime, // 过期时间
sortIndex: expirationTime, // 堆排序用的索引
};
3.3 不同优先级的超时时间
js
var IMMEDIATE_PRIORITY_TIMEOUT = -1; // 立即执行
var USER_BLOCKING_PRIORITY_TIMEOUT = 250; // 250ms
var NORMAL_PRIORITY_TIMEOUT = 5000; // 5s
var LOW_PRIORITY_TIMEOUT = 10000; // 10s
var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt;// 无限期
解释:
- 高优先级(立即) → 马上过期 → 立刻执行
- 普通优先级 → 最长可等 5s
- Idle → 理论上永远不过期,等主线程空闲时再执行
3.4 任务的callback是啥
我们刚才说到:unstable_scheduleCallback
会生成一个 task ,并把它的 callback
存在 task
对象里。那这个 callback
到底一般是什么呢?
task 的 callback 来源
在 React 内部,Scheduler 被 React Fiber 调用,常见的 callback
有:
-
Fiber 渲染任务
-
比如 React 发起一次渲染更新 (
scheduleUpdateOnFiber
) 时,最终会调度一个任务:jsscheduleCallback( NormalPriority, // 或更高优先级 performConcurrentWorkOnRoot.bind(null, root) );
-
这里的
callback
就是performConcurrentWorkOnRoot
,它负责执行 Fiber 的并发渲染流程。
-
-
同步任务
-
当需要同步更新(比如事件处理、flushSync)时,Scheduler 会调度:
jsscheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
-
这里的
callback
就是performSyncWorkOnRoot
,它会直接在本次任务里完成整个 Fiber 渲染,不做时间切片。
-
-
继续执行的续接函数 (continuation)
-
如果一次时间片用完了,但 Fiber 树还没构建完,
performConcurrentWorkOnRoot
会返回一个新的函数(就是 continuation):jsfunction performConcurrentWorkOnRoot(root, didTimeout) { // ... 构建 Fiber 树 if (workInProgress !== null) { // 没做完,返回续接函数 return performConcurrentWorkOnRoot.bind(null, root); } // 做完了返回 null return null; }
-
这样 Scheduler 在下一个时间片会继续执行这个返回的函数,相当于把「未完成的任务」保存了下来,这就是可中断的关键,fiber保存了执行环境,返回的performConcurrentWorkOnRoot可以在后续重新执行。
-
-
其他 React 内部任务
-
比如 被动副作用 (Passive Effects) 的 flush 任务。
-
React 会调用:
jsscheduleCallback(NormalPriority, flushPassiveEffects);
-
此时
callback
就是flushPassiveEffects
。
-
所以总结一下:
- Fiber 渲染主任务 →
performConcurrentWorkOnRoot
(异步,可中断) - 同步渲染任务 →
performSyncWorkOnRoot
(一次完成) - 续接任务 → 上一次未完成的渲染函数继续执行
- 副作用任务 →
flushPassiveEffects
等
4. 任务调度执行
4.1 workLoop
workLoop
是任务真正的消费循环:
js
function workLoop(hasTimeRemaining, initialTime) {
let currentTime = initialTime;
advanceTimers(currentTime); // 把到期的延迟任务转移到 taskQueue
currentTask = peek(taskQueue); // 取队头任务
while (currentTask !== null) {
if (
currentTask.expirationTime > currentTime &&
(!hasTimeRemaining || shouldYieldToHost())
) {
// 任务没过期,但时间片已用完 → 中断
break;
}
const callback = currentTask.callback;
if (typeof callback === 'function') {
currentTask.callback = null;
const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
// 执行任务回调
const continuationCallback = callback(didUserCallbackTimeout);
currentTime = getCurrentTime();
if (typeof continuationCallback === 'function') {
// 返回了续接函数 → 任务没完成,下次继续
currentTask.callback = continuationCallback;
} else {
// 任务完成 → 出队
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
}
advanceTimers(currentTime);
} else {
// 被取消的任务,直接出队
pop(taskQueue);
}
currentTask = peek(taskQueue);
}
// 返回是否还有剩余任务
if (currentTask !== null) {
return true;
} else {
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
return false;
}
}
解释:
- 取出队列头任务(优先级最高)。
- 检查是否要 让出线程(时间片到点/未过期)。
- 执行任务回调,如果回调返回 续接函数,说明任务未完成,挂回队列。
- 如果回调为空(被取消),则跳过。
- 最后返回是否还有剩余任务。
4.2 时间切片退出条件
workLoop
退出有三种情况:
- 队列清空 →
currentTask === null
- 超过时间片 →
shouldYieldToHost() === true
- 任务未过期但时间片已用完 → 暂停,下次再执行
5. 可中断渲染
React 渲染过程中,Fiber 的构建也会被包装成 Scheduler 的任务,因此具备可中断性。
js
function performConcurrentWorkOnRoot(root, didTimeout) {
// ... 渲染 Fiber 树逻辑
if (workInProgress !== null) {
// Fiber 树还没构建完,返回续接函数
return performConcurrentWorkOnRoot.bind(null, root);
} else {
// 构建完成
return null;
}
}
解释:
- 如果任务没做完,返回一个新的函数 → 下次调度继续执行。
- 如果做完了,返回
null
。
这就是 React 实现「可中断渲染」的关键。
6. 任务取消机制
6.1 取消实现
js
function unstable_cancelCallback(task) {
// 标记任务为取消状态
task.callback = null;
}
6.2 原理
- 任务队列是 最小堆,不能直接删除中间节点。
- 所以取消就是 置空 callback。
- 在
workLoop
里会检查,如果 callback 为空就直接跳过。
7. 节流防抖优化
React 内部通过 ensureRootIsScheduled
对任务调度进行优化:
js
function ensureRootIsScheduled(root, currentTime) {
const existingCallbackNode = root.callbackNode;
const nextLanes = getNextLanes(root, workInProgressRootRenderLanes);
const newCallbackPriority = returnNextLanesPriority();
if (nextLanes === NoLanes) {
return; // 没有工作需要调度
}
// 优化逻辑:节流 + 防抖
if (existingCallbackNode !== null) {
const existingCallbackPriority = root.callbackPriority;
if (existingCallbackPriority === newCallbackPriority) {
// 优先级相同 → 复用现有调度(节流)
return;
}
// 优先级不同 → 取消旧任务(防抖)
cancelCallback(existingCallbackNode);
}
// 新建调度任务
let newCallbackNode;
if (newCallbackPriority === SyncLane) {
newCallbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
} else {
const schedulerPriorityLevel = lanesToSchedulerPriority(nextLanes);
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
}
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
}
解释:
- 节流:如果优先级相同,直接复用原任务,不重复调度。
- 防抖:如果优先级更高,取消旧的,换新任务。
8. 调度流程时序图
js
用户交互 / setState
│
├─ scheduleUpdateOnFiber
│ └─ ensureRootIsScheduled
│ └─ unstable_scheduleCallback
│ └─ push(taskQueue, task)
│ └─ requestHostCallback(flushWork)
│
└─ 宿主环境 (MessageChannel / setImmediate)
└─ performWorkUntilDeadline
└─ flushWork
└─ workLoop
├─ 时间切片检查
├─ 执行 task.callback
├─ 检查续接回调
└─ 返回是否有剩余任务
9. 核心特性总结
-
时间切片 (Time Slicing)
- 每片 5ms,避免长任务阻塞 UI。
- 超时/有输入时会让出线程。
-
可中断渲染 (Interruptible Rendering)
- Fiber 渲染任务可以暂停 & 恢复。
- 高优先级任务能打断低优先级任务。
-
优先级调度
- Immediate > UserBlocking > Normal > Low > Idle
- 通过过期时间和最小堆保证正确顺序。
-
性能优化
- 节流防抖避免重复调度。
- 任务取消机制减少无效开销。
✅ 总结
Scheduler 的本质就是:
任务优先级 + 时间切片 + 可中断执行
让 React 能在不阻塞主线程的情况下,流畅地调度渲染和更新。