React 源码阅读 - 调度

481 阅读13分钟

前面的文章中说到了在协调之前还有一个调度的步骤,不过这个调度在目前使用 ReactDOM.render 这个 API的情况下和过去的版本并无区别,React v17 是个过渡版本。

异步可中断模式(Concurrent Mode)下,调度器(Scheduler)才能发挥出其威力,React v18 才会正式支持此模式。

什么是调度?为什么需要调度?

一般来说主流浏览器的刷新频率为 60 Hz,即约每 16.6 ms (1000 ms / 60 Hz) 浏览器就会刷新一次,小于了这个帧率就会让人感到卡顿。

浏览器是单线程的,所以 JS 线程和渲染线程并不能同时执行。

如果代码执行时间比较长(超过了 16.6 ms),那么某一帧或者某几帧就会因为代码执行出现没有时间进行渲染的情况,这时就会出现掉帧的情况,那么就会感受到卡顿。在 React V15 及之前的版本使用的是非 Fiber 的架构,所有 DOM 节点的遍历是用的递归的方式,无法中断又恢复继续执行,所以代码都是同步执行的,就导致了卡顿的问题的存在。

Scheduler2.png

React V16 之后使用了 Fiber 架构,可以将执行时间比较长的任务碎片化。每一个 DOM 节点都对应一个 Fiber,而他们又通过链表的方式连起来,这就可以把遍历方式改成链表深度遍历,记录当前指针就可以实现遍历的暂停与恢复。

Scheduler1.png

如果一个任务耗时比较长一帧执行不为,那么在每一帧就执行一小段任务,然后中断,都留下一些时间来让渲染进程正常执行,下一帧再花点时间来继续执行任务,如此循环往复直到任务执行完。这样每一帧的渲染都没有被阻塞,我们也就不会感受到卡顿了,而这个将任务碎片化,执行,暂停,又执行的过程就叫调度。

Scheduler

调度当然需要调度器,Scheduler 就是当我们配合时间切片,能根据宿主环境性能,为每个工作单元分配一个可运行时间,实现“异步可中断的更新”的调度器。

Scheduler 有两个功能:

  1. 时间切片
  2. 优先级调度

时间切片

除了浏览器重排/重绘,浏览器一帧中可以用于执行 JS 的时机依次是:

宏任务 -> 微任务 -> requestAnimationFrame -> 浏览器重排/重绘 -> requestIdleCallback

requestAnimationFrame 在“浏览器重排/重绘”前执行 JS,这是浏览器渲染之前的最后时机

requestIdleCallback 在“浏览器重排/重绘”后如果当前帧还有空余时间时将被调用

所以时间切片的本质就是模拟实现 requestIdleCallback

为什么要模拟实现呢,因为requestIdleCallback 在浏览器的兼容性上其实是不太友好的,MDN 上也标注其为一个实验中的功能。

事实上 React 是使用宏任务模拟实现 requestIdleCallback的,如果宿主环境支持 MessageChannel 就会使用这个 API,否则会直接使用 setTimeout 来实现。

优先级调度

首先这里的优先级,并不是下面要讲的 Lane 模型的那种优先级,Scheduler 本身也是独立于 React 的包,所以他的优先级也是独立于 React 的优先级的。

Scheduler 的优先级有五种,也分别对应了五个不同的过期时间,任务一旦过期就需要立即执行掉。

大型的 React 项目中可能同时存在很多不同优先级的任务,有些优先级高会被立即执行,有些优先级低会被延时执行,两种任务会被分到不同的队列(timerQueue 保存延时的任务,taskQueue 保存立即执行的任务)。

每当有新的被延时的任务被注册,timerQueue 就会进行重排。

timerQueue 中的任务时间到了我们就将其取出并加入 taskQueue

最后是取出 taskQueue 中最早过去的任务并执行他。

为了能在两个队列中快速地找到最早需要被执行的那个任务,Scheduler 实现了一个小顶堆(SchedulerMinHeap)。我们知道小顶堆的特点是:每个节点的值都小于等于子树中每个节点值,也就是说堆顶就是最小值,对应 Scheduler 中就是堆顶节点就是最早需要被执行的任务,取出他的时间复杂度为 O(1)

源码

scheduleCallback 如何被调用

在前面讲协调的文章里面,讲了 ReactDOM.render 这个 API 的调用过程,不过讲调度器还是得用 ReactDOM.createRoot 这个 API 以使用 Concurrent Mode。所以其调用过程略有不同。其调用过程大致如下图所示:

Scheduler3.png

可以看到最后是调了 schedulerCallback,而且 performConcurrentWorkOnRoot 也是被 这其实就是 Scheduler 中的 unstable_scheduleCallbackunstable 代表现在还不稳定,还在开发中,不是正式版本中的功能(前面也说过要等 React V18 正式版才能用上稳定版的 Concurrent Mode)。

下面看一下 Scheduler的代码,执行过程见注释:

// path: /packages/scheduler/src/forks/Scheduler.js
function unstable_scheduleCallback(priorityLevel, callback, options) {
  // 获取当前时间
  var currentTime = getCurrentTime();

  // 生成开始时间,配置了 delay 就加上延时
  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;
  // 5种优先级以及其对应的5种延时时间,根据优先级配置超时时间
  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;
  }

  // 设置过期时间
  var expirationTime = startTime + timeout;

  // 配置新任务
  var newTask = {
    id: taskIdCounter++,
    callback,
    priorityLevel,
    startTime,
    expirationTime,
    sortIndex: -1,
  };
  if (enableProfiling) {
    newTask.isQueued = false;
  }

  // 开始时间大于当前时间,说明是一个延时任务,还不用开始执行,直接加入 timerQueue
  if (startTime > currentTime) {
    newTask.sortIndex = startTime;
    push(timerQueue, newTask);
    // taskQueue 为空,timerQueue 顶元素就是当前任务,说明所有的任务中,当前任务是最早的
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      if (isHostTimeoutScheduled) {
        cancelHostTimeout();
      } else {
        isHostTimeoutScheduled = true;
      }
      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  } else {
    // 开始时间小于等于当前时间,说明已到开始时间,直接加入 taskQueue
    newTask.sortIndex = expirationTime;
    push(taskQueue, newTask);
    if (enableProfiling) {
      markTaskStart(newTask, currentTime);
      newTask.isQueued = true;
    }
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      // 挂起宏任务,执行渲染任务
      requestHostCallback(flushWork);
    }
  }

  return newTask;
}

requestHostCallback 是一个比较关键的调用,这个函数调用的时候会根据宿主环境的具体情况选择由哪个 API 来执行宏任务,源码中可以看到其优先级顺序为 setImmediate > MessageChannel > setTimeout。为什么是这个顺序呢,因为其执行的延时是逐渐下降的。

setTimeout(fn, 0) 其实是有 4ms 的延时的,setImmediate 虽然是同步任务执行完立马就会执行,但是只有 IENode.JS 才支持,像 Chrome 都不支持,而 MessageChannelChrome 等浏览器上支持,但是在 IE 上又不支持。

flushWork 的调用过程比较复杂,可以进一步看一下 workLoopperformConcurrentWorkOnRoot 函数中的逻辑,总结起来就是不断地取出 taskQueue 中最早过期的任务进行执行,恢复中断继续执行也是在这里去实现的。

performUnitOfWork 中断标志

Concurrent Mode 下,协调阶段始于 performConcurrentWorkOnRoot, 然后会调到 renderRootConcurrent -> workLoopConcurrent 函数。

// path: /packages/react-reconciler/src/ReactFiberWorkLoop.new.js
function workLoopConcurrent() {
  // Perform work until Scheduler asks us to yield
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

shouldYield() 如果为 true 那么循环将会被中断。接下来看一下 shouldYield 是什么。

首先是调到了下面这个地方。

// path: /packages/react-reconciler/src/Scheduler.js
import * as Scheduler from 'scheduler';

export const scheduleCallback = Scheduler.unstable_scheduleCallback;
export const cancelCallback = Scheduler.unstable_cancelCallback;
export const shouldYield = Scheduler.unstable_shouldYield;
export const requestPaint = Scheduler.unstable_requestPaint;
export const now = Scheduler.unstable_now;
export const getCurrentPriorityLevel =
  Scheduler.unstable_getCurrentPriorityLevel;
export const ImmediatePriority = Scheduler.unstable_ImmediatePriority;
export const UserBlockingPriority = Scheduler.unstable_UserBlockingPriority;
export const NormalPriority = Scheduler.unstable_NormalPriority;
export const LowPriority = Scheduler.unstable_LowPriority;
export const IdlePriority = Scheduler.unstable_IdlePriority;
export type SchedulerCallback = (isSync: boolean) => SchedulerCallback | null;

顺藤摸瓜 shouldYield -> unstable_shouldYield -> shouldYieldToHost

// path: /packages/scheduler/src/forks/Scheduler.js
function shouldYieldToHost() {
  if (
    enableIsInputPending &&
    navigator !== undefined &&
    navigator.scheduling !== undefined &&
    navigator.scheduling.isInputPending !== undefined
  ) {
    const scheduling = navigator.scheduling;
    const currentTime = getCurrentTime();
    if (currentTime >= deadline) {
      if (needsPaint || scheduling.isInputPending()) {
        return true;
      }
      const timeElapsed = currentTime - (deadline - yieldInterval);
      return timeElapsed >= maxYieldInterval;
    } else {
      return false;
    }
  } else {
    return getCurrentTime() >= deadline;
  }
}

shouldYieldToHost 函数做的主要事情就是调用 getCurrentTime 函数,获取 currentTime,并和 deadline 对比。当前时间如果小于截止时间就返回 false, 继续 performUnitOfWork 循环执行;如果当前时间大于等于截止时间,那么就需要对比这两个时间的差值以及 yieldInterval 时间来决定是否需要打断 performUnitOfWork 的执行。yieldInterval 默认为 5 ms,会根据实际的宿主环境进行调整,见 Scheduler.jsforceFrameRate 函数,目前 unstable 版本中并未被外部调用。

所以 performUnitOfWork 能否执行全靠 Scheduler 调度,听他指挥。

优先级

优先级相关定义的代码如下:

// path: /packages/scheduler/src/SchedulerPriorities.js
export const NoPriority = 0;
export const ImmediatePriority = 1;
export const UserBlockingPriority = 2;
export const NormalPriority = 3;
export const LowPriority = 4;
export const IdlePriority = 5;

// path: /packages/scheduler/src/forks/Scheduler.js
var maxSigned31BitInt = 1073741823;
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
var USER_BLOCKING_PRIORITY_TIMEOUT = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt;

在之前讲渲染的时候讲过,在 commit 阶段的 before Mutation 阶段,我们执行了 useEffect:

// 路径:packages/react-reconciler/src/ReactFiberWorkLoop.new.js
function commitRootImpl(root, renderPriorityLevel) {
  // 省略一些代码
    if (!rootDoesHavePassiveEffects) {
      rootDoesHavePassiveEffects = true;
      // 异步调用 useEffect
      scheduleCallback(NormalSchedulerPriority, () => {
        flushPassiveEffects();
        return null;
      });
    }
  // 省略一些代码
}

这里的回调便是通过 scheduleCallback 调度的,优先级为 NormalSchedulerPriority,即NormalPriority。最多可被延时 5000ms 执行,这就是为什么我们在这么前面就调用的 useEffect,但是其执行时间却很晚的原因。

Lane 模型

Lane 模型是 React 的优先级系统,是一个控制不同优先级之间的关系与行为的策略。

Lane 原意为赛道,不同的赛车行驶在不同的赛道上,内圈赛道短,外圈赛道长,某几个临近的赛道长度可以看作是一样的。

Lane 模型借鉴了同样的概念,使用 31 为的二进制表示 31 条赛道,位数越小(也就是越靠右,越靠内圈)的赛道优先级越高,某些相邻的赛道拥有相同的优先级。

在开启 Concurrent Mode 的情况下:过期任务或者同步任务的优先级是最高的,使用 SyncLane 赛道;用户交互产生的更新(比如:点击事件)使用较高优先级的赛道;网络请求产生的更新使用一般优先级的赛道;Suspense 使用低优先级的赛道。

源码

赛道定义

SyncLane 赛道优先级最高,优先级逐一降低,OffscreenLane 赛道优先级最低。

// path: packages/react-reconciler/src/ReactFiberLane.new.js
export const TotalLanes = 31;

export const NoLanes: Lanes = /*                        */ 0b0000000000000000000000000000000;
export const NoLane: Lane = /*                          */ 0b0000000000000000000000000000000;

export const SyncLane: Lane = /*                        */ 0b0000000000000000000000000000001;

export const InputContinuousHydrationLane: Lane = /*    */ 0b0000000000000000000000000000010;
export const InputContinuousLane: Lanes = /*            */ 0b0000000000000000000000000000100;

export const DefaultHydrationLane: Lane = /*            */ 0b0000000000000000000000000001000;
export const DefaultLane: Lanes = /*                    */ 0b0000000000000000000000000010000;

const TransitionHydrationLane: Lane = /*                */ 0b0000000000000000000000000100000;
const TransitionLanes: Lanes = /*                       */ 0b0000000001111111111111111000000;
// 省略一些代码

const RetryLanes: Lanes = /*                            */ 0b0000111110000000000000000000000;
// 省略一些代码

export const SomeRetryLane: Lane = RetryLane1;

export const SelectiveHydrationLane: Lane = /*          */ 0b0001000000000000000000000000000;

const NonIdleLanes = /*                                 */ 0b0001111111111111111111111111111;

export const IdleHydrationLane: Lane = /*               */ 0b0010000000000000000000000000000;
export const IdleLane: Lanes = /*                       */ 0b0100000000000000000000000000000;

export const OffscreenLane: Lane = /*                   */ 0b1000000000000000000000000000000;
// 省略一些代码

赛道优先级是如何分配给不同事件的

在使用 createRoot 这个 API 去创建根节点的时候会调用 listenToAllSupportedEvents 去添加所有事件监听,最终会调到 getEventPriority

// path: packages/react-dom/src/events/ReactDOMEventListener.js
export function getEventPriority(domEventName: DOMEventName): * {
  switch (domEventName) {
    case 'cancel':
    case 'click':
    // 省略一些 case
    case 'drop':
    case 'focusin':
    case 'focusout':
    case 'input':
    case 'invalid':
    case 'keydown':
    case 'keypress':
    case 'keyup':
    case 'mousedown':
    case 'mouseup':
    // 省略一些 case
      return DiscreteEventPriority;
    case 'drag':
    case 'dragenter':
    case 'dragexit':
    case 'dragleave':
    case 'dragover':
    case 'mousemove':
    // 省略一些 case
      return ContinuousEventPriority;
    case 'message': {
      const schedulerPriority = getCurrentSchedulerPriorityLevel();
      switch (schedulerPriority) {
        case ImmediateSchedulerPriority:
          return DiscreteEventPriority;
        case UserBlockingSchedulerPriority:
          return ContinuousEventPriority;
        case NormalSchedulerPriority:
        case LowSchedulerPriority:
          return DefaultEventPriority;
        case IdleSchedulerPriority:
          return IdleEventPriority;
        default:
          return DefaultEventPriority;
      }
    }
    default:
      return DefaultEventPriority;
  }
}

事件优先级定义:

// path: packages/react-reconciler/src/ReactEventPriorities.new.js
export const DiscreteEventPriority: EventPriority = SyncLane;
export const ContinuousEventPriority: EventPriority = InputContinuousLane;
export const DefaultEventPriority: EventPriority = DefaultLane;
export const IdleEventPriority: EventPriority = IdleLane;

结合事件优先级的定义可以看出事件的优先级被分成了四类,这四类优先级又对应着四类事件:像 clickkeydownmousedown 等离散事件都是优先级比较高的事件;dragmousemovescroll 之类的连续事件优先级次之。

接下来会根据获取到的事件的优先级分类,设置事件触发时拥有相对应优先级的回调函数。

// path: packages/react-dom/src/events/ReactDOMEventListener.js
export function createEventListenerWrapperWithPriority(
  targetContainer: EventTarget,
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
): Function {
  const eventPriority = getEventPriority(domEventName);
  let listenerWrapper;
  switch (eventPriority) {
    case DiscreteEventPriority:
      listenerWrapper = dispatchDiscreteEvent;
      break;
    case ContinuousEventPriority:
      listenerWrapper = dispatchContinuousEvent;
      break;
    case DefaultEventPriority:
    default:
      listenerWrapper = dispatchEvent;
      break;
  }
  return listenerWrapper.bind(
    null,
    domEventName,
    eventSystemFlags,
    targetContainer,
  );
}

优先级相关计算

赛道优先级的计算都是二级制的位运算。

// path: packages/react-reconciler/src/ReactFiberLane.new.js
// 判断两个赛道是否存在交集
export function includesSomeLane(a: Lanes | Lane, b: Lanes | Lane) {
  return (a & b) !== NoLanes;
}
// 判断某个赛道是不是另一赛道的子集
export function isSubsetOfLanes(set: Lanes, subset: Lanes | Lane) {
  return (set & subset) === subset;
}
// 赛道的或运算,合并俩赛道
export function mergeLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {
  return a | b;
}
// 从一个赛道中移除其某个子赛道
export function removeLanes(set: Lanes, subset: Lanes | Lane): Lanes {
  return set & ~subset;
}
// 获取两个赛道的交集
export function intersectLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {
  return a & b;
}

这些计算会在有更新的时候去判断这个更新的优先级到底够不够,不够的话就可能会被跳过并做一些优先级相关的其它计算,够的话就能正常执行。

React 如何使用 Lane 模型

当我们触发更新之后会调用 requestUpdateLane 函数,以获取当前的赛道,后面的逻辑会根据赛道的信息决定如何去执行。

// path: packages/react-reconciler/src/ReactFiberWorkLoop.new.js
// 请求某个更新的赛道
export function requestUpdateLane(fiber: Fiber): Lane {
  const mode = fiber.mode;
  if ((mode & ConcurrentMode) === NoMode) {
    return (SyncLane: Lane);
  } else if () {
    // 省略一些代码
  }
  // 省略一些代码
}

可以看出在非 ConcurrentMode 模式下,都会使用 SyncLane 赛道,也就是同步赛道。在 Concurrent 模式下,则会返回其对应的赛道。

然后会根据赛道的情况转换成对应的 Scheduler 的优先级,这样就能驱动 Scheduler 的调度了。

// path: packages/react-reconciler/src/ReactFiberWorkLoop.new.js
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
  // 省略一些代码

  let newCallbackNode;
  if (newCallbackPriority === SyncLane) {
    // 省略一些代码
    newCallbackNode = null;
  } else {
    let schedulerPriorityLevel;
    switch (lanesToEventPriority(nextLanes)) {
      case DiscreteEventPriority:
        schedulerPriorityLevel = ImmediateSchedulerPriority;
        break;
      case ContinuousEventPriority:
        schedulerPriorityLevel = UserBlockingSchedulerPriority;
        break;
      case DefaultEventPriority:
        schedulerPriorityLevel = NormalSchedulerPriority;
        break;
      case IdleEventPriority:
        schedulerPriorityLevel = IdleSchedulerPriority;
        break;
      default:
        schedulerPriorityLevel = NormalSchedulerPriority;
        break;
    }
    newCallbackNode = scheduleCallback(
      schedulerPriorityLevel,
      performConcurrentWorkOnRoot.bind(null, root),
    );
  }

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