Skip to content

React Scheduler 源码实现详解

React Scheduler 是一个复杂的系统,为了更好地理解其工作原理,我们将从其核心数据结构、调度循环机制和关键 API 入手,逐层剖析其内部实现。

核心数据结构:任务队列

Scheduler 的高效运作离不开其精心设计的数据结构。它主要使用两个优先级队列来管理任务:

  • taskQueue (任务队列): 存放待执行的任务。这些任务要么是立即执行的,要么是已经到期的延迟任务。这是一个最小堆(Min-Heap),根据任务的 expirationTime(过期时间)进行排序。堆顶始终是最早过期(即最高优先级)的任务。

  • timerQueue (定时器队列): 存放未到期的延迟任务。这也是一个最小堆(Min-Heap),但它是根据任务的 startTime(计划开始时间)进行排序。堆顶是即将到期的任务。

为什么使用最小堆?

选择最小堆作为底层数据结构是性能优化的关键。它提供了两个核心优势:

  1. O(1) 复杂度获取最高优先级任务peek() 操作可以直接访问堆顶元素,无需遍历整个队列。
  2. O(log n) 复杂度插入和删除任务push()pop() 操作能高效地维护堆的有序性,确保任务队列在动态变化时依然能快速找到下一个要执行的任务。

调度循环与协作让步

Scheduler 的核心是它的工作循环(Work Loop),它负责执行任务并决定何时让出主线程,以避免阻塞浏览器渲染。这个过程可以分解为以下几个关键步骤:

1. 请求调度 (requestHostCallback)

当一个新任务被添加到 taskQueue 且当前没有正在进行的调度时,会调用 requestHostCallback。此函数的核心作用是:

  • 异步启动:通过 schedulePerformWorkUntilDeadline,利用 MessageChannel 在浏览器的宏任务队列中安排一个 performWorkUntilDeadline 回调。
  • 保证非阻塞:这确保了 Scheduler 的工作总是在一个独立的宏任务中开始,不会阻塞当前正在执行的同步代码,此时主线程控制权会交还给浏览器。

2. 执行工作循环 (workLoop)

当 Event Loop 执行到 performWorkUntilDeadline 宏任务时,它会调用 flushWork,进而启动 workLoopworkLoop 是一个 while 循环,它不断地从 taskQueuepeek() 出最高优先级的任务并准备执行。

3. 协作让步 (shouldYieldToHost)

在执行每个任务之前,workLoop 都会调用 shouldYieldToHost() 来检查是否应该让出主线程。这是实现时间切片的核心。

  • 判断逻辑shouldYieldToHost() 的判断依据很简单:getCurrentTime() - startTime < frameInterval
  • 时间片控制:它会计算当前时间片从开始到现在已经消耗了多长时间。如果超过了预设的 frameInterval(通常是 5ms),函数返回 true,表示“应该让步”。

4. 执行与中断

根据 shouldYieldToHost() 的返回值,workLoop 会做出不同决策:

  • 不让步 (false): Scheduler 会执行当前任务的回调函数。
    • 任务完成:如果回调执行完毕,任务被 pop() 出队列,循环继续到下一个任务。
    • 任务中断:如果回调返回一个新的函数(称为 “continuation”),表示任务未完成,Scheduler 会保留这个任务,并在下一个时间片继续执行它。
  • 让步 (true): workLoopbreak,暂时停止执行,将控制权交还给浏览器。

5. 重新调度

workLoop 因为时间片用尽而退出时,performWorkUntilDeadline 会检查是否还有未完成的任务。

  • 请求下一次执行:如果有,它会再次调用 requestHostCallback 来安排下一个宏任务,从而在未来的某个时间点继续执行 workLoop
  • 形成循环:这个 “执行 -> 让步 -> 重新调度” 的循环,确保了长时间的 React 更新任务被分解成小块,穿插在浏览器的事件循环中,从而避免了页面卡顿。

这个循环会一直持续,直到 taskQueuetimerQueue 都为空,performWorkUntilDeadline 中的 hasMoreWork 变为 false,调度器进入空闲状态。

通过这种方式,React Scheduler 将长时间的计算任务分解成一系列小的、不超过 frameInterval 的工作单元,在每个单元执行后检查是否需要让出主线程,从而保证了应用的流畅性和响应性。

  • 如果是因为 shouldYieldToHost() 返回 true 而跳出循环,currentTask 此时不为 nullworkLoop 返回 true
  • 如果所有任务都处理完毕 (currentTasknull),workLoop 会检查 timerQueue,如果还有延迟任务,则安排 requestHostTimeout,并返回 false。如果 timerQueue 也为空,返回 false
  • flushWork 返回:
  • workLoop 的返回值作为自己的返回值。
  • finally 块中: isPerformingWork = false;
  • performWorkUntilDeadline 继续执行:
  • hasMoreWork 得到 flushWork 的返回值。
  • 关键点: if (hasMoreWork):
    • 如果为 true (意味着时间片用尽或有任务返回了 continuation),则再次调用 schedulePerformWorkUntilDeadline()
    • 这会将 performWorkUntilDeadline 再次推入下一个宏任务队列,等待浏览器在下一个事件循环中执行。
    • 当前时间切片结束,控制权交还给浏览器。浏览器可以处理用户输入、渲染等。
  • 如果为 false (所有任务完成):
    • isMessageLoopRunning = false; 调度循环结束。
  1. 阶段四:下一个时间切片的开始 (如果需要)
    • 如果前一个时间切片因为 hasMoreWorktrue 而重新调度了 performWorkUntilDeadline
    • (下一个宏任务) 浏览器事件循环再次执行 performWorkUntilDeadline():
      • 流程回到 阶段二,开始一个新的时间切片。startTime 会被重新设置为当前时间,workLoop 会从上次中断的地方(或下一个任务)继续执行。

这个循环会一直持续,直到 taskQueuetimerQueue 都为空,performWorkUntilDeadline 中的 hasMoreWork 变为 false,调度器进入空闲状态。

通过这种方式,React Scheduler 将长时间的计算任务分解成一系列小的、不超过 frameInterval 的工作单元,在每个单元执行后检查是否需要让出主线程,从而保证了应用的流畅性和响应性。

常量

Scheduler 对外暴露了五个优先级的常量 (Priority Constants),这些常量定义了不同任务的紧急程度,Scheduler 会根据这些优先级来决定任务的执行顺序和超时时间。

  • unstable_ImmediatePriority : 最高优先级。
  • unstable_UserBlockingPriority : 用户阻塞型任务的优先级。
  • unstable_NormalPriority : 默认优先级。
  • unstable_LowPriority : 低优先级。
  • unstable_IdlePriority : 最低优先级,用于空闲任务。

这些常量用于传递给 unstable_scheduleCallbackunstable_runWithPriority 等函数,以指定任务的优先级。

API

unstable_scheduleCallback

unstable_scheduleCallback 的主要目标是接收一个回调任务,根据任务的延迟和优先级,策略性地将任务放入不同的等待队列,并利用浏览器的定时器 (setTimeout 概念) 和宏任务 (MessageChannel 概念) 来确保任务在合适的时间被处理,同时兼顾了任务的紧急程度和执行时机。

在其内部中,实现了以下步骤:

  1. 时间计算:清晰地展示了 startTime(考虑延迟)和 expirationTime(基于优先级和开始时间)的计算。
  2. 任务创建:明确了任务对象包含的关键信息。
  3. 队列选择
    • 延迟任务 (startTime > currentTime):进入 timerQueue(按 startTime 排序)。通过 requestHostTimeout (类似 setTimeout) 安排一个未来的检查点 (handleTimeout)。当这个检查点到达时,handleTimeout 会把到期的任务从 timerQueue 移到 taskQueue
    • 立即/到期任务 (startTime <= currentTime):进入 taskQueue(按 expirationTime 排序)。
  4. 触发工作循环
    • 对于放入 taskQueue 的任务,如果当前没有调度工作在进行,会通过 requestHostCallback (通常是 MessageChannel) 来请求浏览器尽快(在下一个宏任务)执行 flushWorkflushWork 负责实际执行 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); }; }

  1. localSetImmediate (Node.js 和旧版 IE):

    • 如果 setImmediate 函数可用(通常在 Node.js 环境或某些旧版 IE 中),Scheduler 会优先使用它。
    • setImmediate 相比 MessageChannel 在 Node.js 环境中有一个优势:它不会阻止 Node.js 进程退出。
    • 它通常比 setTimeout(..., 0) 更早执行,这符合 Scheduler 期望的语义。
  2. MessageChannel (现代浏览器和 Web Worker 环境):

    • 如果 MessageChannel 可用,这是在浏览器环境中的首选方案。
    • MessageChannelpostMessage 可以在当前宏任务执行完毕后,立即将一个新任务推入宏任务队列,有效地避免了 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
  3. 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 ...

schedulePerformWorkUntilDeadline 如何被调用和工作:

  • 它通常由 requestHostCallback 函数调用。requestHostCallback 会检查当前是否已经有一个消息循环在运行 (isMessageLoopRunning),如果没有,它会将 isMessageLoopRunning 设置为 true,然后调用 schedulePerformWorkUntilDeadline() 来启动一个新的工作循环。
  • schedulePerformWorkUntilDeadline 安排 performWorkUntilDeadline 函数执行。
  • performWorkUntilDeadline 函数是 Scheduler 工作循环的实际执行者。它会调用 flushWork 来处理任务队列中的任务。
  • 关键在于,在 performWorkUntilDeadline 函数的 finally 块中,如果 flushWork 返回 true(表示还有更多工作未完成,例如时间切片用尽),它会再次调用 schedulePerformWorkUntilDeadline()。这形成了一个持续的循环:执行一部分工作 -> 如果还有工作 -> 调度下一次执行。

总结来说,schedulePerformWorkUntilDeadline 是一个环境适配的异步调度器,它确保了 React Scheduler 的核心工作循环 (performWorkUntilDeadline) 能够被可靠地、高效地安排在事件循环的后续阶段执行,从而实现了任务的异步处理和时间切片。


微信公众号二维码