Scheduler = 最小堆 + MessageChannel + 时间片检查
它的目标只有一个:推进任务,但永远别饿死浏览器。
调度任务
React 19 最新 Scheduler 源码**(packages/scheduler)** 中,普通任务(非 Immediate,同步优先级之外的任务)的完整调度链:
Plain
unstable_scheduleCallback
↓
requestHostCallback
↓
schedulePerformWorkUntilDeadline
↓
performWorkUntilDeadline
↓
flushWork
↓
workLoop
↓
shouldYieldToHost
↓
advanceTimers
Scheduler 本质是一个"带时间片控制的最小堆 + 宏任务驱动循环"调度器。
一、调度的起点:unstable_scheduleCallback
源码位置(React 19):
Plain
/packages/scheduler/src/forks/Scheduler.js
TypeScript
let getCurrentTime = () => performance.now();
// 为所有任务维护了连个最小堆(每次从队列里面取出来的都是优先级最高(时间即将过期))
// timerQueue // 延时任务队列(task.startTime > now) ------ 按 startTime 排序(延迟最小的先看)
// taskQueue // 立即可执行的任务(task.startTime <= now)------ 按 expirationTime 排序(最紧急的先执行)
// Timeout 对应的值
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
var USER_BLOCKING_PRIORITY_TIMEOUT = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt; // 1073741823
/**
* 调度任务的入口
* @param {*} priorityLevel 优先级等级
* @param {*} callback 任务回调函数
* @param {*} options { delay: number } 该对象有 delay 属性,表示要延迟的时间(决定 expirationTime)
* @returns
*/
function unstable_scheduleCallback(priorityLevel, callback, options) {
// 获取当前的时间
var currentTime = getCurrentTime();
var startTime;
// 设置起始时间 startTime:如果有延时 delay,起始时间需要添加上这个延时,否则起始时间就是当前时间
if (typeof options === "object" && options !== null) {
var delay = options.delay;
if (typeof delay === "number" && delay > 0) {
startTime = currentTime + delay;
} else {
startTime = currentTime;
}
} else {
startTime = currentTime;
}
var timeout;
// 根据传入的优先级等级来设置不同的 timeout
switch (priorityLevel) {
case ImmediatePriority:
timeout = IMMEDIATE_PRIORITY_TIMEOUT; // -1
break;
case UserBlockingPriority:
timeout = USER_BLOCKING_PRIORITY_TIMEOUT; // 250
break;
case IdlePriority:
timeout = IDLE_PRIORITY_TIMEOUT; // 1073741823
break;
case LowPriority:
timeout = LOW_PRIORITY_TIMEOUT; // 10000
break;
case NormalPriority:
default:
timeout = NORMAL_PRIORITY_TIMEOUT; // 5000
break;
}
// 接下来就计算出过期时间
// 只有 ImmediatePriority 任务 比当前时间要早,其他任务都会不同程度的延迟
var expirationTime = startTime + timeout;
// 创建一个新的任务
var newTask = {
id: taskIdCounter++, // 任务 id
callback, // 执行任务回调函数 export type Callback = boolean => ?Callback;
priorityLevel, // 任务的优先级
startTime, // 任务开始时间
expirationTime, // 任务的过期时间
sortIndex: -1, // 用于小顶堆优先级排序,始终从任务队列中拿出最优先的任务
};
if (enableProfiling) {
newTask.isQueued = false;
}
if (startTime > currentTime) {
// 说明这是一个延时任务
newTask.sortIndex = startTime;
// 将该任务推入到 timerQueue 的任务队列中
push(timerQueue, newTask);
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
// 说明 taskQueue 里面的任务已经全部执行完毕,然后从 timerQueue 里面取出一个优先级最高的任务作为此时的 newTask
if (isHostTimeoutScheduled) {
cancelHostTimeout();
} else {
isHostTimeoutScheduled = true;
}
// 如果是延时任务,调用 requestHostTimeout 进行延时任务的调度
requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
// 说明不是延时任务
newTask.sortIndex = expirationTime; // 设置了 sortIndex 后,在任务队列里面进行一个排序
push(taskQueue, newTask);
if (enableProfiling) {
markTaskStart(newTask, currentTime);
newTask.isQueued = true;
}
// 最终调用 requestHostCallback 进行普通任务的调度
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
requestHostCallback();
}
}
// 向外部返回任务
return newTask;
}
它的职责是:
- 根据优先级计算 timeout
- 构造一个 Task 对象
- 放入任务到对应的最小堆
- 请求宿主开始调度(requestHostCallback 普通任务 / requestHostTimeout 延时任务)
任务执行时刻:
IMMEDIATE_PRIORITY_TIMEOUT -> currentTime -> USER_BLOCKING_PRIORITY_TIMEOUT -> IDLE_PRIORITY_TIMEOUT
二、普通任务调用 requestHostCallback
当普通任务进入 taskQueue 后,调用 requestHostCallback,然后调用 schedulePerformWorkUntilDeadline:
TypeScript
/**
*
* @param {*} callback 是在调用的时候传入的 flushWork
* requestHostCallback 这个函数没有做什么事情,主要就是调用 schedulePerformWorkUntilDeadline
*/
function requestHostCallback() {
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
schedulePerformWorkUntilDeadline();// 实例化 MessageChannel 进行后面的调度
}
}
let schedulePerformWorkUntilDeadline; // undefined
if (typeof localSetImmediate === 'function') {
// Node.js and old IE.
schedulePerformWorkUntilDeadline = () => {
localSetImmediate(performWorkUntilDeadline);
};
} else if (typeof MessageChannel !== 'undefined') {
// 大多数情况下,使用的是 MessageChannel
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
schedulePerformWorkUntilDeadline = () => {
port.postMessage(null);
};
} else {
// setTimeout 进行兜底
schedulePerformWorkUntilDeadline = () => {
localSetTimeout(performWorkUntilDeadline, 0);
};
}
schedulePerformWorkUntilDeadline 根据不同的环境选择不同的生成宏任务的方式。大多数都是 MessageChannel :
- 每一次调度推进
- 都是一个新的宏任务
- 浏览器中间有机会 paint
三、延时任务调用 requestHostTimeout & handleTimeout
React Scheduler 不直接使用 setTimeout,而是抽象成 HostConfig,可在不同环境实现。
requestHostTimeout(callback, ms) 这个函数会安排一个底层超时回调。
在浏览器环境下它一般等价于:
JavaScript
const timeoutID = setTimeout(callback, ms);
注意:这是 严格意义上的延时调度,用于把 timerQueue 的任务唤醒进 taskQueue。
requestHostTimeout 实际上就是调用 setTimoutout,然后在 setTimeout 中,调用传入的 handleTimeout。
注意:延时任务只需要"不会提前执行",而不需要"精准执行",所以这里使用了 setTimeout,setTimeout 只是负责"唤醒" Scheduler,不负责精度和优先级(expirationTime 控制)控制,鉴于负责延时和必须是宏任务的特性,这里使用 setTimeout 最合适。
React 的调度精度来自:MessageChannel + 时间片检查 + expirationTime 。
handleTimeout 是真正被 setTimeout 调用的函数:
TypeScript
function handleTimeout(currentTime) {
// 遍历 timerQueue,将时间已经到了的延时任务放入到 taskQueue
advanceTimers(currentTime);
if (!isHostCallbackScheduled) {
if (taskQueue.length > 0) {
// 采用调度普通任务的方式进行调度
requestHostCallback(flushWork);
} else {
// 如果任务仍然是延时,继续设置 HostTimeout
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
const nextDelay = firstTimer.startTime - currentTime;
requestHostTimeout(handleTimeout, nextDelay);
}
}
}
}
四、performWorkUntilDeadline
这是 Scheduler 的"驱动心跳"。
TypeScript
let startTime = -1;
const performWorkUntilDeadline = () => {
if (enableRequestPaint) {
needsPaint = false;
}
if (isMessageLoopRunning) {
const currentTime = getCurrentTime();
// 这里的 startTime 并非 unstable_scheduleCallback 方法里面的 startTime
// 而是一个全局变量,默认值为 -1
// 用来测量任务的执行时间,从而能够知道主线程被阻塞了多久
startTime = currentTime;
let hasMoreWork = true;
try {
// flushWork 为任务中转,本质上内部继续调用 workLoop 判断任务执行情况
hasMoreWork = flushWork(currentTime);
} finally {
if (hasMoreWork) {
// 上面刚刚讲过的方法,根据不同的环境选择不同的生成宏任务的方式(MessageChannel)
schedulePerformWorkUntilDeadline();
} else {
isMessageLoopRunning = false;
}
}
}
};
它做三件事:
- 设置本次调度的时间起点
- 调用 flushWork
- 如果还有任务 → 再发一个 MessageChannel
五、flushWork 和 workLoop 执行任务循环
TypeScript
// flushWork 是个中转,它真正执行的是 workLoop
function flushWork(initialTime) {
// ...
// 各种开关、报错捕获...
// 其实核心就是 workLoop
return workLoop(initialTime);
}
// 不断从任务队列中取出任务执行
function workLoop(initialTime: number) {
// initialTime 开始执行任务的时间
let currentTime = initialTime;
// advanceTimers 是用来遍历 timerQueue,判断是否有已经到期的任务
// 如果有,将这个任务放入到 taskQueue
advanceTimers(currentTime);
currentTask = peek(taskQueue);
while (currentTask !== null) {
if (!enableAlwaysYieldScheduler) {
if (currentTask.expirationTime > currentTime && shouldYieldToHost()) {
// 任务没有过期,并且需要中断任务,归还主线程
break;
}
}
// 返回任务本身,致使任务之后可以接着继续执行
const callback = currentTask.callback;
if (typeof callback === 'function') {
currentTask.callback = null;
currentPriorityLevel = currentTask.priorityLevel;
const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
if (enableProfiling) {
markTaskRun(currentTask, currentTime);
}
// 执行任务,其他判断都是做"阶段过程资料收集",无需关注
const continuationCallback = callback(didUserCallbackTimeout);
currentTime = getCurrentTime();
if (typeof continuationCallback === 'function') {
currentTask.callback = continuationCallback;
if (enableProfiling) {
markTaskYield(currentTask, currentTime);
}
advanceTimers(currentTime);
return true;
} else {
if (enableProfiling) {
markTaskCompleted(currentTask, currentTime);
currentTask.isQueued = false;
}
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
advanceTimers(currentTime);
}
} else {
pop(taskQueue);
}
// 执行完,再从 taskQueue 中取出一个任务
currentTask = peek(taskQueue);
if (enableAlwaysYieldScheduler) {
if (currentTask === null || currentTask.expirationTime > currentTime) {
break;
}
}
}
// 如果任务不为空,那么还有更多的任务,hasMoreTask 为 true
if (currentTask !== null) {
return true;
} else {
// taskQueue 这个队列空了,那么我们就从 timerQueue 里面去看延时任务
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
// 没有进入上面的 if,说明 timerQueue 里面的任务也完了,返回 false,外部的 hasMoreWork 拿到的就也是 false
return false;
}
}
任务是否可以被打断?
TypeScript
shouldYieldToHost()
如果返回 true:
- 当前宏任务结束
- 重新发 MessageChannel
- 浏览器可以渲染
六、shouldYieldToHost ------ 时间片控制核心
源码核心:
TypeScript
function shouldYieldToHost() {
const timeElapsed = getCurrentTime() - startTime;
if (timeElapsed < frameInterval) {
return false;
}
return true;
}
其中:
TypeScript
frameInterval = 5ms
注意:
React 不等 16ms 一整帧
而是:
每 5ms 检查一次
为什么?
- 预留给浏览器 layout + paint
- 预留给输入事件
七、advanceTimers ------ 延迟任务晋升机制
每次 workLoop 结束后,会调用 advanceTimers(currentTime); 。
它会:
- 检查 timerQueue
- 把到时间的任务移动到 taskQueue
TypeScript
function advanceTimers(currentTime) {
// 取出 startTime <= currentTime 的 timer
let timer = peek(timerQueue);
while (timer !== null && timer.startTime <= currentTime) {
pop(timerQueue);
timer.sortIndex = timer.expirationTime;
push(taskQueue, timer);
timer = peek(timerQueue);
}
}
这保证:延迟任务不会饿死
八、Scheduler 与 Fiber 的关系
Scheduler 不知道 Fiber。
它只负责:"什么时候调用 callback"
而 callback 通常是:
Plain
performConcurrentWorkOnRoot
那里面才是:
- beginWork
- completeWork
- commitRoot
九、任务拆分的思路来源
假设有这样一段源码:
JavaScript
unstable_scheduleCallback(NormalPriority, () => {
heavyWork(); // 10ms
});
如果 heavyWork() 是同步执行 10ms:,Scheduler 无法中断它,因为 JS 是单线程的,函数执行过程中无法被抢占。
来看源码核心逻辑:
JavaScript
const continuation = callback();
if (typeof continuation === 'function') {
currentTask.callback = continuation;
} else {
pop(taskQueue);
}
如果 callback 返回一个函数,Scheduler 会把它当成"任务的下一段"。
举一个最小可理解示例:
JavaScript
// 假设这是一个"大"任务
function createBigTask(total) {
let i = 0;
function work() {
while (i < total) {
console.log(i++);
if (shouldStopManually()) {
return work; // 👈 返回自身,表示还有后续
}
}
return null; // 完成
}
return work;
}
调度:
JavaScript
unstable_scheduleCallback(
NormalPriority,
createBigTask(1000)
);
执行:
Plain
workLoop()
↓
执行 work()
↓
执行到一部分
↓
返回 work (continuation)
↓
Scheduler 保存 callback = work
↓
shouldYieldToHost() 为 true
↓
break
↓
下一帧继续执行
在 React 里,被拆分的任务不是"普通函数",而是:performConcurrentWorkOnRoot。
它内部会调用:workLoopConcurrent() 。
核心逻辑(简化版):
JavaScript
while (
workInProgress !== null &&
!shouldYield()
) {
performUnitOfWork(workInProgress);
}
performUnitOfWork 只做一个 Fiber 节点:
JavaScript
function performUnitOfWork(unit) {
const next = beginWork(unit);
if (next === null) {
completeUnitOfWork(unit);
}
return next;
}
也就是说:React 把整棵 Fiber 树拆成一个个"节点单位"/"任务切片"。
更形象的可以表示为:
一个组件树:
Plain
App
├─ Header
├─ Content
│ ├─ List
│ └─ Sidebar
└─ Footer
会被拆成:
Plain
performUnitOfWork(App)
performUnitOfWork(Header)
performUnitOfWork(Content)
performUnitOfWork(List)
performUnitOfWork(Sidebar)
performUnitOfWork(Footer)
每个 Fiber 节点就是一个小任务。
回到 Scheduler 这段判断:
JavaScript
if (
currentTask.expirationTime > currentTime &&
shouldYieldToHost()
) {
break;
}
当时间片用完时:break 。
此时:
- workLoop 停止
- 任务还没完成
- currentTask.callback 仍然是 performConcurrentWorkOnRoot
下一帧:
Plain
MessageChannel
↓
performWorkUntilDeadline
↓
flushWork
↓
workLoop
↓
继续执行 performConcurrentWorkOnRoot
延时任务并不会立刻进入 taskQueue,而是先进入 timerQueue,等待被"唤醒"然后再调度。
十、完整调度流程
下面是一个完整的流程图:
Plain
unstable_scheduleCallback()
↓
是否延时? (startTime > now)
/ \
是 否
↓ ↓
push(timerQueue) push(taskQueue)
↓ ↓
requestHostTimeout(requestHostCallback)
↓ ↓
等待 Timer requestHostCallback
↓ ↓
timeout 到时 MessageChannel
↓ ↓
handleTimeout performWorkUntilDeadline
↓ ↓
advanceTimers flushWork
↓ ↓
timerQueue → taskQueue
↓
workLoop
↓
shouldYieldToHost?
↓
是 否
notifyBrowserPaint 执行 Callback
↓
callback 返回 continuation?
↓
是 否
更新 Task.callback pop(taskQueue)
↓
可能再次调度
示例理解
示例 1:普通任务(正常进入队列)
JavaScript
unstable_scheduleCallback(
NormalPriority,
() => console.log("normal")
);
步骤:
- now <= startTime (0 delay)
- 放入 taskQueue
- requestHostCallback(flushWork)
- MessageChannel 触发 performWorkUntilDeadline
- flushWork → workLoop
- 执行 task
- taskQueue 空 → 调度结束
示例 2:延时任务(未来才执行)
JavaScript
unstable_scheduleCallback(
NormalPriority,
() => console.log("delayed"),
{ delay: 1000 }
);
步骤:
Plain
t=0
↓
startTime = now + 1000
push(timerQueue)
requestHostTimeout(handleTimeout, 1000)
1s 后:
Plain
handleTimeout 被 setTimeout 调用
↓
advanceTimers
→ 取出 timerQueue 里所有 startTime <= 1s 的任务
→ 推到 taskQueue
taskQueue 变非空
↓
requestHostCallback(flushWork)
↓
MessageChannel 回调
↓
flushWork → workLoop → task 执行
示例 3:延时任务到了中间又被打断
JavaScript
unstable_scheduleCallback(
NormalPriority,
step1,
{ delay: 1000 }
);
function step1() {
console.log("step1");
return step2;
}
function step2() {
console.log("step2");
}
执行过程:
Plain
t=0 → timerQueue 入队
t=1000 → handleTimeout
→ advanceTimers → taskQueue
↓
MessageChannel
↓
workLoop 执行 step1 延时任务
↓
step1 返回 continuation step2 延时任务
↓
此时
workLoop 检查 shouldYieldToHost
如果 time片到 → 中断
→ callback (continuation) 留在 taskQueue
→ requestHostCallback 执行普通任务
下次继续执行 step2 延时任务
设计哲学
① 延时任务不能抢占浏览器渲染
如果立即调度:
Plain
setTimeout(() => {}), Promise.then(...)
React 可能会阻塞页面渲染
所以必须分开:
Plain
先 timerQueue 等待
再 taskQueue 才跑
② 延时任务按照优先级
即使 delay 到了:
Plain
expirationTime = startTime + timeout
如果更高优先级任务进入 taskQueue:
React 会先执行高优先级的。
delay控制什么时候进入 taskQueue,而expirationTime控制进入 taskQueue 之后的优先级排序。
③ extendable 继续推进任务
一个任务返回 continuation 时:
Plain
currentTask.callback = continuation;
并且还可能继续 schedule。