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) 能够被可靠地、高效地安排在事件循环的后续阶段执行,从而实现了任务的异步处理和时间切片。
