React的任务调度器-Scheduler

11/11/2023 React

在 React 18 中,React 将铺垫了两个版本的并发渲染模式正式转正,默认开启该模式。React 18 引入了全新的任务调度器,作为并发渲染模式的一部分。该调度器不仅是 React 的一部分,还是一个独立的开源库,为外部提供使用。本文将深入讨论 React 18 的任务调度器,包括任务单元、调度方式以及调度过程。

# 任务单元

任务调度器中,任务是最小的调度单元,通过优先队列的数据结构排列。每个任务包含了唯一的 ID、回调函数、优先级以及过期时间等信息。React 使用最小堆(优先队列)的方式进行任务排序,确保按照优先级和过期时间执行。

var newTask: Task = {
  id: taskIdCounter++, // 唯一id
  callback, // 任务的回调执行
  priorityLevel, // 任务的优先级
  startTime, // 任务的开始时间
  expirationTime, // 任务的过期时间
  sortIndex: -1, // 堆的排序index, 跟过期时间的值一致
};

任务调度器共有五个优先级,每个优先级对应不同的过期时间,调度器按照过期时间对任务进行排序,优先执行最早过期的任务。

// Times out immediately
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
// Eventually times out
var USER_BLOCKING_PRIORITY_TIMEOUT = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
// Never times out
var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt;

switch (priorityLevel) {
  case ImmediatePriority:
    timeout = IMMEDIATE_PRIORITY_TIMEOUT;
    break;
  case UserBlockingPriority:
    timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
    break;
  case IdlePriority:
    timeout = IDLE_PRIORITY_TIMEOUT;
    break;
  case LowPriority:
    timeout = LOW_PRIORITY_TIMEOUT;
    break;
  case NormalPriority:
  default:
    timeout = NORMAL_PRIORITY_TIMEOUT;
    break;
}

# 调度方式

由于 JavaScript 引擎是单线程执行的,执行过程中是不能中断并且会阻塞渲染或者其他计算。调度器为了实现中断的效果,通过宏任务的方式去执行分片。根据环境不同,可能使用**setImmediate(nodejs或ie)MessageChannel(优先)setTimeout(兜底)**等方式。因为**setTimeout**就算是延迟 0 秒,浏览器还是默认给 4ms 的间隔去执行。

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);
  };
}

# 调度过程

在调度器中,拥有两个队列(堆),一个是任务队列,一个延迟队列。当调用调度方法(unstable_scheduleCallback)的时候传入的第三个参数指定 delay 的时候,该任务才会进入延迟队列,否则进入任务队列。

进行调度循环的时候,因为队列顶部任务的过期时间都是最快的,所以会获取顶部的任务判断该任务是否过期,把所有过期的任务都执行。剩下没过期的任务,先判断 startTime 和 currTime 比较是否大于 5ms(在之前的版本中,会根据浏览器的 fps 去进行判断,最新版本直接写死 5ms 了),如果是,就执行,否则进入延迟队列的判断,把已经超过延迟时间的任务都取出来放入任务队列中,继续循环。如果都没有任务,这次调度结束,等待下一次的调度。

function workLoop(initialTime: number) {
  let currentTime = initialTime;
  advanceTimers(currentTime);
  currentTask = peek(taskQueue);
  while (
    currentTask !== null &&
    !(enableSchedulerDebugging && isSchedulerPaused)
  ) {
    if (currentTask.expirationTime > currentTime && shouldYieldToHost()) {
      // This currentTask hasn't expired, and we've reached the deadline.
      break;
    }
    // $FlowFixMe[incompatible-use] found when upgrading Flow
    const callback = currentTask.callback;
    if (typeof callback === 'function') {
      // $FlowFixMe[incompatible-use] found when upgrading Flow
      currentTask.callback = null;
      // $FlowFixMe[incompatible-use] found when upgrading Flow
      currentPriorityLevel = currentTask.priorityLevel;
      // $FlowFixMe[incompatible-use] found when upgrading Flow
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      if (enableProfiling) {
        // $FlowFixMe[incompatible-call] found when upgrading Flow
        markTaskRun(currentTask, currentTime);
      }
      const continuationCallback = callback(didUserCallbackTimeout);
      currentTime = getCurrentTime();
      if (typeof continuationCallback === 'function') {
        // If a continuation is returned, immediately yield to the main thread
        // regardless of how much time is left in the current time slice.
        // $FlowFixMe[incompatible-use] found when upgrading Flow
        currentTask.callback = continuationCallback;
        if (enableProfiling) {
          // $FlowFixMe[incompatible-call] found when upgrading Flow
          markTaskYield(currentTask, currentTime);
        }
        advanceTimers(currentTime);
        return true;
      } else {
        if (enableProfiling) {
          // $FlowFixMe[incompatible-call] found when upgrading Flow
          markTaskCompleted(currentTask, currentTime);
          // $FlowFixMe[incompatible-use] found when upgrading Flow
          currentTask.isQueued = false;
        }
        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
        advanceTimers(currentTime);
      }
    } else {
      pop(taskQueue);
    }
    currentTask = peek(taskQueue);
  }
  // Return whether there's additional work
  if (currentTask !== null) {
    return true;
  } else {
    const firstTimer = peek(timerQueue);
    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false;
  }
}

# 结语

任务调度器为 React 带来了更高效的任务处理机制。通过任务单元的最小化、多优先级的排序、中断式调度等特性,React 在不同环境中都能提供流畅的用户体验。