React源码系列——三分钟了解React Scheduler是如何工作的

166 阅读5分钟

React Scheduler 工作原理解析

这份代码是 React 的调度器(Scheduler)实现,主要负责协调和安排各种任务的执行优先级和时机。React 的 Scheduler 是 React 并发模式的核心部分,它允许 React 实现时间切片(time slicing)和优先级调度,使 React 能够中断渲染以响应更高优先级的用户交互。

源码地址:github.com/facebook/re…

思维导图:

image.png (图源:github.com/7kms/react-…

核心概念

1. 任务优先级

Scheduler 定义了5种优先级:

  • ImmediatePriority: 最高优先级,需要立即执行
  • UserBlockingPriority: 用户阻塞优先级,如用户输入、点击等交互
  • NormalPriority: 普通优先级,大多数工作的默认优先级
  • LowPriority: 低优先级
  • IdlePriority: 最低优先级,只在浏览器空闲时执行

每种优先级对应不同的超时时间,决定任务可以延迟多久。

2. 任务队列

Scheduler 维护两个主要队列:

  • taskQueue: 存放已经可以执行的任务
  • timerQueue: 存放延迟执行的任务

这两个队列都是最小堆结构,使得优先级最高的任务总是在堆顶。

关键方法解析

workLoop

workLoop 是调度器的核心循环,负责从任务队列中取出任务并执行:

function workLoop(initialTime) {
  let currentTime = initialTime;
  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;
      
      // 执行任务回调
      const continuationCallback = callback(didUserCallbackTimeout);
      currentTime = getCurrentTime();
      
      if (typeof continuationCallback === 'function') {
        // 如果回调返回了一个函数,说明任务需要继续执行
        currentTask.callback = continuationCallback;
        advanceTimers(currentTime);
        return true;
      } else {
        // 任务已完成
        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
        advanceTimers(currentTime);
      }
    } else {
      pop(taskQueue);
    }
    currentTask = peek(taskQueue);
    if (enableAlwaysYieldScheduler) {
      if (currentTask === null || currentTask.expirationTime > currentTime) {
        break;
      }
    }
  }
  
  // 返回是否还有更多工作
  return currentTask !== null;
}

workLoop 的主要工作:

  1. 将到期的定时任务从 timerQueue 移到 taskQueue
  2. taskQueue 中取出最高优先级任务执行
  3. 如果浏览器需要处理其他工作,则中断循环让出控制权
  4. 如果任务返回一个函数,说明任务需要继续执行,将其放回队列

advanceTimers

function advanceTimers(currentTime) {
  // 检查不再延迟的任务并添加到主队列
  let timer = peek(timerQueue);
  while (timer !== null) {
    if (timer.callback === null) {
      // 任务被取消
      pop(timerQueue);
    } else if (timer.startTime <= currentTime) {
      // 任务可以执行了,转移到任务队列
      pop(timerQueue);
      timer.sortIndex = timer.expirationTime;
      push(taskQueue, timer);
      if (enableProfiling) {
        markTaskStart(timer, currentTime);
        timer.isQueued = true;
      }
    } else {
      // 剩余计时器仍在等待中
      return;
    }
    timer = peek(timerQueue);
  }
}

这个方法检查 timerQueue 中的延迟任务,如果已经到了开始时间,就将其移动到 taskQueue

unstable_scheduleCallback

function unstable_scheduleCallback(
  priorityLevel,
  callback,
  options,
) {
  const currentTime = getCurrentTime();

  // 确定任务开始时间
  let startTime;
  if (typeof options === 'object' && options !== null) {
    const delay = options.delay;
    if (typeof delay === 'number' && delay > 0) {
      startTime = currentTime + delay;
    } else {
      startTime = currentTime;
    }
  } else {
    startTime = currentTime;
  }

  // 根据优先级确定超时时间
  let timeout;
  switch (priorityLevel) {
    case ImmediatePriority:
      timeout = -1; // 立即超时
      break;
    case UserBlockingPriority:
      timeout = userBlockingPriorityTimeout;
      break;
    case IdlePriority:
      timeout = maxSigned31BitInt; // 永不超时
      break;
    case LowPriority:
      timeout = lowPriorityTimeout;
      break;
    case NormalPriority:
    default:
      timeout = normalPriorityTimeout;
      break;
  }

  const expirationTime = startTime + timeout;

  // 创建新任务
  const newTask = {
    id: taskIdCounter++,
    callback,
    priorityLevel,
    startTime,
    expirationTime,
    sortIndex: -1,
  };

  if (startTime > currentTime) {
    // 这是一个延迟任务,放入定时器队列
    newTask.sortIndex = startTime;
    push(timerQueue, newTask);
    
    // 如果这是最早的延迟任务,设置一个超时来处理它
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      if (isHostTimeoutScheduled) {
        cancelHostTimeout();
      } else {
        isHostTimeoutScheduled = true;
      }
      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  } else {
    // 这是一个立即执行的任务,放入任务队列
    newTask.sortIndex = expirationTime;
    push(taskQueue, newTask);
    
    // 安排一个主线程回调来执行任务
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      requestHostCallback();
    }
  }

  return newTask;
}

这个方法用于安排一个新任务,根据任务的优先级和延迟时间,将其放入不同的队列并安排适当的执行时机。

shouldYieldToHost

function shouldYieldToHost() {
  if (!enableAlwaysYieldScheduler && enableRequestPaint && needsPaint) {
    // 需要绘制,立即让出控制权
    return true;
  }
  const timeElapsed = getCurrentTime() - startTime;
  if (timeElapsed < frameInterval) {
    // 主线程阻塞时间很短,小于一帧,暂不让出
    return false;
  }
  // 让出控制权
  return true;
}

这个方法决定是否应该暂停当前工作,让浏览器有机会处理其他任务(如渲染、用户输入等)。它基于两个因素:

  1. 是否需要绘制(needsPaint
  2. 当前任务已经执行了多长时间(是否超过了帧间隔)

调度算法

React Scheduler 的核心调度算法基于以下几个关键点:

1. 优先级队列

使用最小堆数据结构实现优先级队列,确保高优先级任务先执行。任务按照以下方式排序:

  • 对于 timerQueue,按照 startTime 排序
  • 对于 taskQueue,按照 expirationTime 排序

2. 时间切片

Scheduler 实现了时间切片,通过 shouldYieldToHost 方法来决定是否应该中断当前工作。默认情况下,如果执行时间超过了一帧的时间(约16.6ms),就会中断执行,让浏览器有机会处理其他工作。

3. 延续和中断

任务可以返回一个函数,表示需要继续执行。这允许长时间运行的任务被分割成多个较小的部分,每个部分可以在不同的时间片中执行,从而避免阻塞主线程。

4. 平台适配

Scheduler 使用不同的机制来安排回调,优先使用:

  1. setImmediate(如果可用)
  2. MessageChannel(大多数现代浏览器)
  3. setTimeout(最后的备选方案)

其中 MessageChannel 是最常用的,因为它不受4ms最小超时限制(setTimeout有这个限制)。

总结

React Scheduler 是 React 并发模式的核心部分,允许 React:

  1. 根据优先级安排不同的工作
  2. 将长时间运行的工作分割成小块
  3. 在浏览器需要处理用户交互或渲染时让出控制权
  4. 在浏览器空闲时继续执行低优先级工作

这种设计使 React 能够保持应用的响应性,即使在进行大量计算工作的情况下,也能及时响应用户交互。