本文仅仅是阅读 图解 React 原理系列 的笔记,了解更多内容请查看原文链接。

调度原理

调度实现

调度中心最核心的代码,在 scheduler/src/forks/SchedulerHostConfig.default.js 中。该文件共导出 8 个函数。

/** 请求及时回调 */
export let requestHostCallback;
/** 取消及时回调 */
export let cancelHostCallback;
/** 请求延时回调 */
export let requestHostTimeout;
/** 取消延时回调 */
export let cancelHostTimeout;
/** 是否让出主线程 */
export let shouldYieldToHost;
/** 请求绘制 */
export let requestPaint;
/** 获取当前时间 */
export let getCurrentTime;
/** 强制设置 yieldInterval */
export let forceFrameRate;

调度相关

与调度相关的,请求或取消调度,它们的目的是请求执行(或取消)回调函数

  • requestHostCallback
  • cancelHostCallback
  • requestHostTimeout
  • cancelHostTimeout
const performWorkUnitlDeadline = () => {
  if (scheduledHostCallback !== null) {
    const currentTime = getCurrentTime();
    // 在 yieldInterval 毫秒后产生,无论我们在 vsync 周期中的哪个位置
    // 这意味着在消息时间开始时总是有剩余时间
    deadline = currentTime + yieldInterval;
    const hasTimeRemaining = true;
    try {
      // 执行回调,返回是否还有剩余任务
      const hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
      if (!hasMoreWork) {
        isMessageLoopRunning = false;
        scheduledHostCallback = null;
      } else {
        // 如果还有更多的工作,在前一个消息事件的末尾安排下一个消息事件
        port.postMessage(null);
      }
    } catch (error) {
      // 如果调度任务抛错,退出当前浏览器任务以便观察错误
      port.postMessage(null);
      throw error;
    }
  } else {
    isMessageLoopRunning = false;
  }
  needsPaint = false;
};

const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUnitlDeadline;

requestHostCallback = function (callback) {
  scheduledHostCallback = callback;
  if (!isMessageLoopRunning) {
    isMessageLoopRunning = true;
    port.postMessage(null);
  }
};

cancelHostCallback = function () {
  scheduledHostCallback = null;
};

requestHostTimeout = function (callback, ms) {
  taskTimeoutId = setTimeout(() => {
    callback(getCurrentTime());
  }, ms);
};

cancelHostTimeout = function () {
  clearTimeout(taskTimeoutId);
  taskTimeoutId = -1;
};

请求回调之后 scheduledHostCallback = callback,然后通过 MessageChannel 发消息的方式触发 performWorkUnitlDeadline,最后执行回调 scheduledHostCallback。

需要注意:MessageChannel 在浏览器事件循环中属于宏任务,所以调度中心用于是异步执行回调函数。

时间切片

时间切片(time slicing),执行时间分割,让出主线程(把控制权归返浏览器,浏览器可以处理用户输入,UI 绘制等紧急任务)

  • getCurrentTime
  • shouldYieldToHost
  • requestPaint
  • forceFrameRate
if (
  enableIsInputPending &&
  navigator !== undefined &&
  navigator.scheduler !== undefined &&
  navigator.scheduler.isInputPending !== undefined
) {
  const scheduling = navigator.scheduling;
  shouldYieldToHost = function () {
    const currentTime = getCurrentTime();
    if (currentTime >= deadline) {
      if (needsPaint || scheduling.isInputPending()) {
        return true;
      }
      return currentTime >= maxYieldInterval;
    } else {
      return false;
    }
  };
  requestPaint = function () {
    needsPaint = true;
  };
} else {
  shouldYieldToHost = function () {
    return getCurrentTime >= deadline;
  };
  requestPaint = function () {};
}

forceFrameRate = function (fps) {
  if (fps < 0 || fps > 125) {
    console['error'](
      'forceFrameRate takes a positive int between 0 and 125, ' +
        'forcing frame rates higher than 125 fps is not supported'
    );
    return;
  }
  if (fps > 0) {
    yieldInterval = Math.floor(1000 / fps);
  } else {
    yieldInterval = 5;
  }
};

shouldYieldToHost 的判定条件:

  • currentTime >= deadline(currentTime + yieldInterval),只有时间超过 deadline 之后才会让出主线程。
    • yieldInterval 默认是 5ms,只能通过 forceFrameRate 函数进行修改。
    • 如果一个 task 运行时间超过 5ms,下一个 task 执行之前,会把控制权归还浏览器。
  • navigator.scheduling.isInputPending,用于判断是否有输入事件。

从上面可以得到调度中心的内核实现图:

任务队列管理

调度的目的是为了消费任务,接下来看看任务队列的管理和实现。

scheduler/src/Scheduler.js 维护一个 taskQueue,任务队列管理主要就是围绕 taskQueue 展开。

// 任务队列保存在一个最小堆里
var taskQueue = [];
var timerQueue = [];

创建任务

scheduler/src/Scheduler.js 中的 unstable_schedulerCallback 函数:

function unstable_schedulerCallback(priorityLevel, callback, options) {
  var currentTime = getCurrentTime;

  var startTime;

  if (typeof options === 'object' && options !== null) {
    var delay = options.delay;
    if (typeof delay === 'number' && delay > 0) {
      startTime = currentTime + delay;
    } else {
      startTime = currentTime;
    }
  } else {
    startTime = currentTime;
  }

  var timeout;

  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 LowPirority:
      timeout = LOW_PRIORITY_TIMEOUT;
      break;
    case NormalPriority:
    default:
      timeout = NORMAL_PRIORITY_TIMEOUT;
      break;
  }

  var expirationTime = startTime + timeout;

  var 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(flushWork);
    }
  }
  return newTask;
}

消费任务

创建任务之后,最后请求调度 requestHostCallback(flushWork),flushWork 函数作为参数传入调度中心内核等待回调。

function flushWork(hasTimeRemaining, initialTime) {
  // 省略无关代码
  // 做好全局标记,表示现在已经进入调度阶段
  isHostCallbackScheduled = false;
  if (isHostTimeoutScheduled) {
    isHostTimeoutScheduled = false;
    cancelHostTimeout();
  }

  isPerformingWork = true;
  const previousPriorityLevel = currentPriorityLevel;

  try {
    // 循环消费队列
    return workLoop(hasTimeRemaining, initialTime);
  } finally {
    // 还原全局标记
    currentTask = null;
    currentPriorityLevel = previousPriorityLevel;
    isPerformingWork = false;
  }
}

flushWork 中调用了 workLoop,队列消费的主要逻辑是在 workLoop 函数中,这就是 React 的任务调度循环。

function workLoop(hasTimeRemaining, initialTime) {
  let currentTime = initialTime;
  advanceTimers(currentTime);
  currentTask = peek(taskQueue);
  while (currentTask !== null) {
    if (currentTask.expirationTime > currentTask && (!hasTimeRemaining || 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;
      } else {
        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
      }
      advanceTimers(currentTime);
    } else {
      pop(taskQueue);
    }
    currentTask = peek(taskQueue);
  }

  // 返回,不管是否有额外的工作
  if (currentTask !== null) {
    return true;
  } else {
    const firstTimer = peek(timerQueue);
    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false;
  }
}

workLoop 就是一个大循环,在此处实现了时间切片和 fiber 树的可中断渲染。这 2 个大特性的实现,都集中于这个 while 循环。

每一次 while 循环的推出就是一个时间切片,深入分析 while 循环的退出条件:

  1. 队列被完全清空:正常的退出。
  2. 执行超时:在消费 taskQueue 时,在执行 task.callback 之前,都会检测是否超时,所以超时检测是以 task 为单位。

    如果某个 task.callback 执行时间太久也会超时(如 fiber 树过大,逻辑很重),这时使用检测机制,超时了立刻暂停 task.callback 的执行

时间切片原理

消费任务队列的过程中,可以消费 1~n 个 task,甚至清空整个 queue。但是在每一次具体执行 task.callback 之前都要进行检测,如果超时可以立即推出循环并等待下一次调用。

可中断渲染

在时间切片的基础之上,如果单个 task.callback 执行时间就很长(假设 200ms)。就需要 task.callback 自己能够检测是否超时,所以在 fiber 树构建过程中,每构建完成一个单元,都会检测一次超时,如遇超时就会退出 fiber 树循环构建,并返回一个新的回调函数(就是 continuationCallback)并等待下一次回调继续未完成的 fiber 树构造。

节流防抖

在 reconciler 运作流程中的总结的 4 个阶段中,注册调度任务属于第 2 个阶段,核心逻辑位于 react-reconciler/src/ReactFiberWorkLoop.new.js ensureRootIsScheduled 函数中。

function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
  const existingCallbackNode = root.callbackNode;

  // 检测是否有车道被其他工作占用,如果有,将它们标记为已过期
  markStarvedLanesAsExpired(root, currentTime);

  // 确定下一个要处理的的车道及其优先级
  const nextLanes = getNextLanes(
    root,
    root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes
  );

  if (nextLanes === NoLanes) {
    if (existingCallbackNode !== null) {
      cancelCallback(existingCallbackNode);
      root.callbackNode = null;
      root.callbackPriority = NoLanePriority;
    }
    return;
  }

  // 节流防抖
  if (existingCallbackNode !== null) {
    const existingCallbackPriority = root.callbackPriority;
    if (existingCallbackPriority === newCallbackPriority) {
      return;
    }
    cancelCallback(existingCallbackNode);
  }

  // 调度一个新的任务
  let newCallbackNode;
  if (newCallbackPriority === SyncLanePriority) {
    newCallbackNode = scheduledHostCallback(performSyncWorkOnRoot.bind(null, root));
  } else if (newCallbackPriority === SyncBatchedLanePriority) {
    newCallbackNode = scheduleCallback(
      ImmediateSchedulerPriority,
      performSyncWorkOnRoot.bind(null, root)
    );
  } else {
    const schedulerPriorityLevel = lanePriorityToSchedulerPriority(newCallbackPriority);
    newCallbackNode = scheduleCallback(
      schedulerPriorityLevel,
      performConcurrentWorkOnRoot.bind(null, root)
    );
  }

  root.callbackPriority = newCallbackPriority;
  root.callbackNode = newCallbackNode;
}

正常的情况下,ensureRootIsScheduled 函数会与 scheduler 包通信,最后注册一个 task 并等待回调。

  1. 在 task 注册完成之后,会设置 FiberRoot 对象上的属性,代表现在已经处于调度进行中。
  2. 再次进入 ensureRootIsScheduled 时,如果发现处于调度中,则需要一些节流和防抖措施,进而保证调度性能。

    1. 节流(判断条件:existingCallbackPriority === newCallbackPriority,新旧更新的优先级相同),则无需注册新 task,直接退出调用。
    2. 防抖(判断条件:existingCallbackPriority !== newCallbackPriority,新旧更新的优先级不同),则取消旧 task,重新注册新 task。

参考链接