Appearance
React Scheduler 源码实现详解
React Scheduler 是一个复杂的系统,为了更好地理解其工作原理,我们将从其核心数据结构、调度循环机制和关键 API 入手,逐层剖析其内部实现。
核心数据结构:任务队列
Scheduler 的高效运作离不开其精心设计的数据结构。它主要使用两个优先级队列来管理任务:
taskQueue
(任务队列): 存放待执行的任务。这些任务要么是立即执行的,要么是已经到期的延迟任务。这是一个最小堆(Min-Heap),根据任务的expirationTime
(过期时间)进行排序。堆顶始终是最早过期(即最高优先级)的任务。timerQueue
(定时器队列): 存放未到期的延迟任务。这也是一个最小堆(Min-Heap),但它是根据任务的startTime
(计划开始时间)进行排序。堆顶是即将到期的任务。
为什么使用最小堆?
选择最小堆作为底层数据结构是性能优化的关键。它提供了两个核心优势:
- O(1) 复杂度获取最高优先级任务:
peek()
操作可以直接访问堆顶元素,无需遍历整个队列。 - O(log n) 复杂度插入和删除任务:
push()
和pop()
操作能高效地维护堆的有序性,确保任务队列在动态变化时依然能快速找到下一个要执行的任务。
调度循环与协作让步
Scheduler 的核心是它的工作循环(Work Loop),它负责执行任务并决定何时让出主线程,以避免阻塞浏览器渲染。这个过程可以分解为以下几个关键步骤:
1. 请求调度 (requestHostCallback
)
当一个新任务被添加到 taskQueue
且当前没有正在进行的调度时,会调用 requestHostCallback
。此函数的核心作用是:
- 异步启动:通过
schedulePerformWorkUntilDeadline
,利用MessageChannel
在浏览器的宏任务队列中安排一个performWorkUntilDeadline
回调。 - 保证非阻塞:这确保了 Scheduler 的工作总是在一个独立的宏任务中开始,不会阻塞当前正在执行的同步代码,此时主线程控制权会交还给浏览器。
2. 执行工作循环 (workLoop
)
当 Event Loop 执行到 performWorkUntilDeadline
宏任务时,它会调用 flushWork
,进而启动 workLoop
。workLoop
是一个 while
循环,它不断地从 taskQueue
中 peek()
出最高优先级的任务并准备执行。
3. 协作让步 (shouldYieldToHost
)
在执行每个任务之前,workLoop
都会调用 shouldYieldToHost()
来检查是否应该让出主线程。这是实现时间切片的核心。
- 判断逻辑:
shouldYieldToHost()
的判断依据很简单:getCurrentTime() - startTime < frameInterval
。 - 时间片控制:它会计算当前时间片从开始到现在已经消耗了多长时间。如果超过了预设的
frameInterval
(通常是 5ms),函数返回true
,表示“应该让步”。
4. 执行与中断
根据 shouldYieldToHost()
的返回值,workLoop
会做出不同决策:
- 不让步 (
false
): Scheduler 会执行当前任务的回调函数。- 任务完成:如果回调执行完毕,任务被
pop()
出队列,循环继续到下一个任务。 - 任务中断:如果回调返回一个新的函数(称为 “continuation”),表示任务未完成,Scheduler 会保留这个任务,并在下一个时间片继续执行它。
- 任务完成:如果回调执行完毕,任务被
- 让步 (
true
):workLoop
会break
,暂时停止执行,将控制权交还给浏览器。
5. 重新调度
当 workLoop
因为时间片用尽而退出时,performWorkUntilDeadline
会检查是否还有未完成的任务。
- 请求下一次执行:如果有,它会再次调用
requestHostCallback
来安排下一个宏任务,从而在未来的某个时间点继续执行workLoop
。 - 形成循环:这个 “执行 -> 让步 -> 重新调度” 的循环,确保了长时间的 React 更新任务被分解成小块,穿插在浏览器的事件循环中,从而避免了页面卡顿。
这个循环会一直持续,直到 taskQueue
和 timerQueue
都为空,performWorkUntilDeadline
中的 hasMoreWork
变为 false
,调度器进入空闲状态。
通过这种方式,React Scheduler 将长时间的计算任务分解成一系列小的、不超过 frameInterval
的工作单元,在每个单元执行后检查是否需要让出主线程,从而保证了应用的流畅性和响应性。
- 如果是因为
shouldYieldToHost()
返回true
而跳出循环,currentTask
此时不为null
,workLoop
返回true
。 - 如果所有任务都处理完毕 (
currentTask
为null
),workLoop
会检查timerQueue
,如果还有延迟任务,则安排requestHostTimeout
,并返回false
。如果timerQueue
也为空,返回false
。 flushWork
返回:- 将
workLoop
的返回值作为自己的返回值。 - 在
finally
块中:isPerformingWork = false;
performWorkUntilDeadline
继续执行:hasMoreWork
得到flushWork
的返回值。- 关键点:
if (hasMoreWork)
:- 如果为
true
(意味着时间片用尽或有任务返回了 continuation),则再次调用schedulePerformWorkUntilDeadline()
。 - 这会将
performWorkUntilDeadline
再次推入下一个宏任务队列,等待浏览器在下一个事件循环中执行。 - 当前时间切片结束,控制权交还给浏览器。浏览器可以处理用户输入、渲染等。
- 如果为
- 如果为
false
(所有任务完成):isMessageLoopRunning = false;
调度循环结束。
- 阶段四:下一个时间切片的开始 (如果需要)
- 如果前一个时间切片因为
hasMoreWork
为true
而重新调度了performWorkUntilDeadline
: - (下一个宏任务) 浏览器事件循环再次执行
performWorkUntilDeadline()
:- 流程回到 阶段二,开始一个新的时间切片。
startTime
会被重新设置为当前时间,workLoop
会从上次中断的地方(或下一个任务)继续执行。
- 流程回到 阶段二,开始一个新的时间切片。
- 如果前一个时间切片因为
这个循环会一直持续,直到 taskQueue
和 timerQueue
都为空,performWorkUntilDeadline
中的 hasMoreWork
变为 false
,调度器进入空闲状态。
通过这种方式,React Scheduler 将长时间的计算任务分解成一系列小的、不超过 frameInterval
的工作单元,在每个单元执行后检查是否需要让出主线程,从而保证了应用的流畅性和响应性。
常量
Scheduler
对外暴露了五个优先级的常量 (Priority Constants),这些常量定义了不同任务的紧急程度,Scheduler
会根据这些优先级来决定任务的执行顺序和超时时间。
unstable_ImmediatePriority
: 最高优先级。unstable_UserBlockingPriority
: 用户阻塞型任务的优先级。unstable_NormalPriority
: 默认优先级。unstable_LowPriority
: 低优先级。unstable_IdlePriority
: 最低优先级,用于空闲任务。
这些常量用于传递给 unstable_scheduleCallback
或 unstable_runWithPriority
等函数,以指定任务的优先级。
API
unstable_scheduleCallback
unstable_scheduleCallback
的主要目标是接收一个回调任务,根据任务的延迟和优先级,策略性地将任务放入不同的等待队列,并利用浏览器的定时器 (setTimeout
概念) 和宏任务 (MessageChannel
概念) 来确保任务在合适的时间被处理,同时兼顾了任务的紧急程度和执行时机。
在其内部中,实现了以下步骤:
- 时间计算:清晰地展示了
startTime
(考虑延迟)和expirationTime
(基于优先级和开始时间)的计算。 - 任务创建:明确了任务对象包含的关键信息。
- 队列选择:
- 延迟任务 (
startTime > currentTime
):进入timerQueue
(按startTime
排序)。通过requestHostTimeout
(类似setTimeout
) 安排一个未来的检查点 (handleTimeout
)。当这个检查点到达时,handleTimeout
会把到期的任务从timerQueue
移到taskQueue
。 - 立即/到期任务 (
startTime <= currentTime
):进入taskQueue
(按expirationTime
排序)。
- 延迟任务 (
- 触发工作循环:
- 对于放入
taskQueue
的任务,如果当前没有调度工作在进行,会通过requestHostCallback
(通常是MessageChannel
) 来请求浏览器尽快(在下一个宏任务)执行flushWork
,flushWork
负责实际执行taskQueue
中的任务。 - 对于
timerQueue
,当其顶端任务的startTime
到达时,handleTimeout
被触发,它会将任务移至taskQueue
,并可能接着触发requestHostCallback
。
- 对于放入
内部实现
javascript
function unstable_scheduleCallback(priorityLevel, callback, options) {
// 1. 获取当前时间
const currentTime = getCurrentTime();
// 2. 计算任务的开始时间 (startTime)
// 如果 options 中指定了 delay,则 startTime = currentTime + delay
// 否则,startTime = currentTime (立即开始)
let startTime;
if (options != null && options.delay != null && options.delay > 0) {
startTime = currentTime + options.delay;
} else {
startTime = currentTime;
}
// 3. 根据优先级计算任务的超时时间 (timeout)
// 不同优先级对应不同的超时时长,例如 ImmediatePriority 的超时时间很短或没有,
// 而 LowPriority 的超时时间会比较长。
const timeout = timeoutForPriorityLevel[priorityLevel];
// 4. 计算任务的过期时间 (expirationTime)
// expirationTime = startTime + timeout
const expirationTime = startTime + timeout;
// 5. 创建任务对象 (newTask)
// 任务对象至少包含: id, callback, priorityLevel, startTime, expirationTime, sortIndex (通常是 expirationTime 或 startTime)
const newTask = {
id: taskIdCounter++, // 唯一的任务 ID
callback: callback, // 要执行的回调函数
priorityLevel: priorityLevel, // 任务的优先级
startTime: startTime, // 任务的计划开始时间
expirationTime: expirationTime, // 任务的过期时间
// sortIndex 用于在队列中排序,对于 timerQueue 是 startTime,对于 taskQueue 是 expirationTime
};
// 6. 将任务加入队列
if (startTime > currentTime) {
// 这是一个延迟任务 (delayed task)
// - 设置 newTask.sortIndex = startTime
// - 将 newTask 加入 timerQueue (一个按 startTime 排序的最小堆)
// - 如果 newTask 成为了 timerQueue 中最早开始的任务 (即堆顶任务),
// 并且当前没有正在等待的 hostTimeout,则调用 requestHostTimeout(handleTimeout, startTime - currentTime)
// 来安排一个 setTimeout,在 startTime 时刻执行 handleTimeout。
// handleTimeout 的作用是将到期的延迟任务从 timerQueue 移动到 taskQueue。
newTask.sortIndex = startTime;
push(timerQueue, newTask);
if (peek(timerQueue) === newTask && !isHostTimeoutScheduled) {
isHostTimeoutScheduled = true;
requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
// 这是一个立即任务 (immediate task) 或已到期的延迟任务
// - 设置 newTask.sortIndex = expirationTime
// - 将 newTask 加入 taskQueue (一个按 expirationTime 排序的最小堆)
// - 如果当前没有正在进行的调度工作 (isPerformingWork 为 false) 且没有回调被请求 (isHostCallbackScheduled 为 false),
// 则调用 requestHostCallback(flushWork) 来请求浏览器在下一个宏任务中执行 flushWork。
// flushWork 会开始处理 taskQueue 中的任务。
newTask.sortIndex = expirationTime;
push(taskQueue, newTask);
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);
}
}
// 7. 返回任务对象,以便可以取消它
return newTask;
}
unstable_cancelCallback
unstable_cancelCallback 函数
function unstable_cancelCallback(task: Task) { if (enableProfiling) { if (task.isQueued) { const currentTime = getCurrentTime(); markTaskCanceled(task, currentTime); task.isQueued = false; } }
// Null out the callback to indicate the task has been canceled. (Can't // remove from the queue because you can't remove arbitrary nodes from an // array based heap, only the first one.) task.callback = null; }
schedulePerformWorkUntilDeadline
schedulePerformWorkUntilDeadline
是 React Scheduler 内部用于异步调度任务执行的核心函数。它的主要职责是确保 performWorkUntilDeadline
函数能够在下一个事件循环的宏任务(macrotask)中被调用,从而启动或继续 Scheduler 的工作循环。
schedulePerformWorkUntilDeadline
的实现会根据当前 JavaScript 环境选择最优的异步调度方式:
schedulePerformWorkUntilDeadline 内部实现
let schedulePerformWorkUntilDeadline; if (typeof localSetImmediate === 'function') { // Node.js and old IE. // There's a few reasons for why we prefer setImmediate. // // Unlike MessageChannel, it doesn't prevent a Node.js process from exiting. // (Even though this is a DOM fork of the Scheduler, you could get here // with a mix of Node.js 15+, which has a MessageChannel, and jsdom.) // https://github.com/facebook/react/issues/20756 // // But also, it runs earlier which is the semantic we want. // If other browsers ever implement it, it's better to use it. // Although both of these would be inferior to native scheduling. schedulePerformWorkUntilDeadline = () => { localSetImmediate(performWorkUntilDeadline); }; } else if (typeof MessageChannel !== 'undefined') { // DOM and Worker environments. // We prefer MessageChannel because of the 4ms setTimeout clamping. const channel = new MessageChannel(); const port = channel.port2; channel.port1.onmessage = performWorkUntilDeadline; schedulePerformWorkUntilDeadline = () => { port.postMessage(null); }; } else { // We should only fallback here in non-browser environments. schedulePerformWorkUntilDeadline = () => { // $FlowFixMe[not-a-function] nullable value localSetTimeout(performWorkUntilDeadline, 0); }; }
localSetImmediate
(Node.js 和旧版 IE):- 如果
setImmediate
函数可用(通常在 Node.js 环境或某些旧版 IE 中),Scheduler 会优先使用它。 setImmediate
相比MessageChannel
在 Node.js 环境中有一个优势:它不会阻止 Node.js 进程退出。- 它通常比
setTimeout(..., 0)
更早执行,这符合 Scheduler 期望的语义。
- 如果
MessageChannel
(现代浏览器和 Web Worker 环境):- 如果
MessageChannel
可用,这是在浏览器环境中的首选方案。 MessageChannel
的postMessage
可以在当前宏任务执行完毕后,立即将一个新任务推入宏任务队列,有效地避免了setTimeout(..., 0)
可能存在的最小 4ms 延迟(尽管浏览器对此有所优化,但MessageChannel
仍然被认为是更精确和高效的宏任务调度方式)。- 实现:javascript这里,
// ... existing code ... } else if (typeof MessageChannel !== 'undefined') { // DOM and Worker environments. // We prefer MessageChannel because of the 4ms setTimeout clamping. const channel = new MessageChannel(); const port = channel.port2; channel.port1.onmessage = performWorkUntilDeadline; schedulePerformWorkUntilDeadline = () => { port.postMessage(null); }; // ... existing code ...
port2.postMessage(null)
会触发port1.onmessage
事件,从而执行performWorkUntilDeadline
。
- 如果
localSetTimeout(performWorkUntilDeadline, 0)
(降级方案):- 如果以上两种方式都不可用,Scheduler 会降级使用
setTimeout(performWorkUntilDeadline, 0)
。这是一个通用的异步调度方法,可以在几乎所有 JavaScript 环境中工作。 - 实现:javascript
// ... existing code ... } else { // We should only fallback here in non-browser environments. schedulePerformWorkUntilDeadline = () => { // $FlowFixMe[not-a-function] nullable value localSetTimeout(performWorkUntilDeadline, 0); }; } // ... existing code ...
- 如果以上两种方式都不可用,Scheduler 会降级使用
schedulePerformWorkUntilDeadline
如何被调用和工作:
- 它通常由
requestHostCallback
函数调用。requestHostCallback
会检查当前是否已经有一个消息循环在运行 (isMessageLoopRunning
),如果没有,它会将isMessageLoopRunning
设置为true
,然后调用schedulePerformWorkUntilDeadline()
来启动一个新的工作循环。 schedulePerformWorkUntilDeadline
安排performWorkUntilDeadline
函数执行。performWorkUntilDeadline
函数是 Scheduler 工作循环的实际执行者。它会调用flushWork
来处理任务队列中的任务。- 关键在于,在
performWorkUntilDeadline
函数的finally
块中,如果flushWork
返回true
(表示还有更多工作未完成,例如时间切片用尽),它会再次调用schedulePerformWorkUntilDeadline()
。这形成了一个持续的循环:执行一部分工作 -> 如果还有工作 -> 调度下一次执行。
总结来说,schedulePerformWorkUntilDeadline
是一个环境适配的异步调度器,它确保了 React Scheduler 的核心工作循环 (performWorkUntilDeadline
) 能够被可靠地、高效地安排在事件循环的后续阶段执行,从而实现了任务的异步处理和时间切片。
