这下简历上应该可以写读过React源码了吧(持续更新中...)

863 阅读27分钟

本文中的 react 版本为 16.6.0,虽然目前 react 已经到了 18 版本,和 16 版本有不小的差异,其中最大的差异应该就是 18 版本开启Concurrent Mode,请注意 开启 这个词,Concurrent Mode 并非是在 18 版本中才出现的,在 16 版本中基于 Fiber 架构的时间分片的调度(schedule)方式已经做了实现,只是通过 ReactDOM.render 的方式并不能开启 Concurrent Mode。在阅读 react16 源码的过程中可以看到很多 ”未来式“ 的代码,这些都是组成了 react18 的重要基石,并且能够深入理解 16版本 这个全新架构的逻辑流程,对以后学习 react18 的源码起到了很好的过渡作用,并能达到循序渐进的效果。

本文并不能起到教学的作用,更多的是作为学习笔记,会对一些难懂的代码逻辑增加解读注释,所以还是尽量为大家起到一个文档查询的作用。

在越往后的源码学习中,可能会对之前看到的源码有新的认识和理解,之前代码的解读注释也会有所更新变更,每次发布前我会在文章开头说明下更新内容。

话不多说,开始吧!

更新列表

20220914

  1. 更新 4.3findHighestPriorityRoot 方法说明。
  2. 更新 5.2addRootToSchedule 方法说明。
  3. 新增 5.4performWork 方法说明。
  4. 总结第 5 节中的全局变量。

1. 入口文件

ReactDOM.render(
  <App />,
  document.getElementById('root'),
  callback
)

调用 ReactDOM.render 方法,传入入口 JSX、要挂载到的 dom 节点及 callback(本文暂不考虑传入 callback 的情况)。

2. ReactDOM.render

文件路径:react-dom/src/client/ReactDOM.js

创建 ReactDOM 对象

const ReactDOM: Object = {
  render(
    element: React$Element<any>,
    container: DOMContainer,
    callback: ?Function,
  ) {
    return legacyRenderSubtreeIntoContainer(
      null,
      element,
      container,
      false,
      callback,
    );
  },
}

legacyRenderSubtreeIntoContainer

function legacyRenderSubtreeIntoContainer(
  parentComponent: ?React$Component<any, any>,
  children: ReactNodeList, // <App/>
  container: DOMContainer,
  forceHydrate: boolean,
  callback: ?Function,
) {

  let root: Root = (container._reactRootContainer: any);
  root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
    container,
    forceHydrate,
  );
  // Initial mount should not be batched.
  DOMRenderer.unbatchedUpdates(() => {
    root.render(children, callback);
  });
  return DOMRenderer.getPublicRootInstance(root._internalRoot);
}

根节点的渲染,parentComponent 值为 nullforceHydratefalsehydrate 涉及到 SSR ,所以先默认认为均为 false 。然后legacyCreateRootFromDOMContainer 会创建一个 ReactRoot 类的实例,然后再调用实例上的 render 方法。这里 unbatchedUpdates 方法的作用就是执行一次传入的root.render() 方法 ,只是在 render 执行前将全局变量 isUnbatchingUpdates 置为 true,执行后置为 false

function legacyCreateRootFromDOMContainer(
  container: DOMContainer,
  forceHydrate: boolean,
): Root {
  const shouldHydrate = forceHydrate; // false
  if (!shouldHydrate) {
    let rootSibling;
    while ((rootSibling = container.lastChild)) {
      container.removeChild(rootSibling);
    }
  }
  // Legacy roots are not async by default.
  const isConcurrent = false;
  return new ReactRoot(container, isConcurrent, shouldHydrate);
}

SSR 渲染的情况下,会清空要挂载的节点下的所有子节点。根节点的渲染,react 目前使用的是 legacy 模式。

function ReactRoot(
  container: Container, // #root节点
  isConcurrent: boolean, // false
  hydrate: boolean, // false
) {
  const root = DOMRenderer.createContainer(container, isConcurrent, hydrate);
  this._internalRoot = root;
}

ReactRoot 类会调用 react-reconciler/inline.dom 中的 createContainer 方法创建 FiberRoot

3. 创建 FiberRoot

文件路径:react-reconciler/inline.dom

function createContainer(
  containerInfo: Container, // root dom节点
  isConcurrent: boolean, // false
  hydrate: boolean, // false
): OpaqueRoot {
  return createFiberRoot(containerInfo, isConcurrent, hydrate);
}
function createFiberRoot(
  containerInfo: any, // #root
  isConcurrent: boolean, // false
  hydrate: boolean, // false
): FiberRoot {
  // Cyclic construction. This cheats the type system right now because
  // stateNode is any.
  const uninitializedFiber = createHostRootFiber(isConcurrent);
  let root;
  root = ({
    current: uninitializedFiber,
    containerInfo: containerInfo,
    pendingChildren: null,

    earliestPendingTime: NoWork,
    latestPendingTime: NoWork,
    earliestSuspendedTime: NoWork,
    latestSuspendedTime: NoWork,
    latestPingedTime: NoWork,

    didError: false,

    pendingCommitExpirationTime: NoWork,
    finishedWork: null,
    timeoutHandle: noTimeout,
    context: null,
    pendingContext: null,
    hydrate,
    nextExpirationTimeToWorkOn: NoWork,
    expirationTime: NoWork,
    firstBatch: null,
    nextScheduledRoot: null,

    interactionThreadID: unstable_getThreadID(),
    memoizedInteractions: new Set(),
    pendingInteractionMap: new Map(),
  }: FiberRoot);

  uninitializedFiber.stateNode = root;
  return ((root: any): FiberRoot);
}

createFiberRoot 方法会生成一个 FiberRoot 对象,该对象上的 current 属性对应一个叫 RootFiber 的根 fiber 结构,也是一个对象,该对象的 stateNode 又会指向 FiberRoot。 这里可以先记住这两个属性的作用。

3.1 RootFiber

function createHostRootFiber(isConcurrent: boolean): Fiber {
  let mode = isConcurrent ? ConcurrentMode | StrictMode : NoContext
  return createFiber(HostRoot, null, null, mode);
}

HostRoot 是一个常量,固定值为 3,代表当前 fiber 的类型是根 fiber,当前传入的 isConcurrentfalse,所以 mode 值为 NoContextNoContext 是一个二进制常量 0b000,代表当前更新方式的类型。

3.2 Fiber

const createFiber = function(
  tag: WorkTag, // HostRoot 3
  pendingProps: mixed, // null
  key: null | string, // null
  mode: TypeOfMode, // legend mode 0b000
): Fiber {
  // $FlowFixMe: the shapes are exact here but Flow doesn't like constructors
  return new FiberNode(tag, pendingProps, key, mode);
};

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // Instance
  this.tag = tag;
  this.key = key;
  this.elementType = null;
  this.type = null;
  this.stateNode = null;

  // Fiber
  this.return = null;
  this.child = null;
  this.sibling = null;
  this.index = 0;

  this.ref = null;

  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;
  this.firstContextDependency = null;

  this.mode = mode;

  // Effects
  this.effectTag = NoEffect;
  this.nextEffect = null;

  this.firstEffect = null;
  this.lastEffect = null;

  this.expirationTime = NoWork;
  this.childExpirationTime = NoWork;

  this.alternate = null;
}

每个 React 节点都会对应一个fiber结构,其中 return 指向父节点,child 指向第一个孩子节点,sibling 指向兄弟节点,相当于是一个节点只有一个子节点,遍历子节点时只需遍历 sibling 即可。这里同样也是有很多属性,后续也会提到。

4. 创建更新

4.1 方法调用

ReactRoot.prototype.render = function(
  children: ReactNodeList,
  callback: ?() => mixed,
): Work {
  const root = this._internalRoot;
  const work = new ReactWork();
  callback = callback === undefined ? null : callback;
  if (callback !== null) {
    work.then(callback);
  }
  DOMRenderer.updateContainer(children, root, null, work._onCommit);
  return work;
};

上一节 legacyRenderSubtreeIntoContainer 方法调用了 ReactRootrender 方法 DOMRenderer.updateContainer 会创建一个更新队列。

4.2 updateContainer

function updateContainer(
  element: ReactNodeList, // <App/>
  container: OpaqueRoot, // FiberRoot
  parentComponent: ?React$Component<any, any>, // null
  callback: ?Function,
): ExpirationTime {
  const current = container.current;
  const currentTime = requestCurrentTime();
  const expirationTime = computeExpirationForFiber(currentTime, current);
  return updateContainerAtExpirationTime(
    element,
    container,
    parentComponent,
    expirationTime,
    callback,
  );
}

该方法做了两件事:1. 计算传入节点的过期时间(当前为根节点,如果是 setState,则传入的是当前节点)。2.用计算得到的过期时间创建更新。这里 currentFiberRoot 上的 current 属性,指向 RootFiber

4.3 计算过期时间 expirationTime

expirationTime 有几个固定值,分别如下:

const MAX_SIGNED_31_BIT_INT = 1073741823;
const NoWork = 0; // 表示无更新
const Sync = 1; // 同步更新,最高优先级
const Never = MAX_SIGNED_31_BIT_INT; // V8中32位系统的最大整数

expirationTime 的作用是为了标记各个节点的更新优先级,来决定当前更新是否立即执行还是异步执行。先看下 currentTime 的大致计算过程:

function requestCurrentTime() {
  if (isRendering) {
    return currentSchedulerTime;
  }
  findHighestPriorityRoot();
  if (
    nextFlushedExpirationTime === NoWork ||
    nextFlushedExpirationTime === Never
  ) {
    recomputeCurrentRendererTime();
    currentSchedulerTime = currentRendererTime;
    return currentSchedulerTime;
  }
  return currentSchedulerTime;
}

isRenderingperformWorkOnRoot 的时候会被置位 true,所以此时处于rendering 的时候,直接返回之前记录的 currentTime ,即在一次 rendering 中产生的更新,计算过期时间要用到的 currentTime 必须和此次更新的 currentTime 保持一致。

findHighestPriorityRoot

function findHighestPriorityRoot() {
  let highestPriorityWork = NoWork;
  let highestPriorityRoot = null;
  if (lastScheduledRoot !== null) {
    ...
  }
  nextFlushedRoot = highestPriorityRoot;
  nextFlushedExpirationTime = highestPriorityWork;
}

lastScheduledRoot 表示最后一个要被调度的 FiberRoot 节点。首次渲染根节点的时候,全局变量 lastScheduledRoot 初始值为 null,所以根本不会进入判断逻辑,这里可以先认为啥也没做。lastScheduledRoot 被赋值是在下一章节调度中的 addRootToSchedule 方法中被赋值的,在 addRootToSchedule 那一节我会回过头来再说明一下这个方法的作用,这里可以先跳过。

recomputeCurrentRendererTime

let originalStartTimeMs: number = now();
function recomputeCurrentRendererTime() {
  const currentTimeMs = now() - originalStartTimeMs;
  currentRendererTime = msToExpirationTime(currentTimeMs);
}
const UNIT_SIZE = 10;
const MAGIC_NUMBER_OFFSET = 2;
function msToExpirationTime(ms: number): ExpirationTime {
  // Always add an offset so that we don't clash with the magic number for NoWork.
  return ((ms / UNIT_SIZE) | 0) + MAGIC_NUMBER_OFFSET;
}

originalStartTimeMs 相当于记录了react应用开始加载的时间,是一个固定值,后续的更新以此时间点计算过期时间。

now() 类似 Date.now 或者 performance.now| 0 是取整的用途,ms / UNIT_SIZE) | 0 意思是 10ms 之内产生的所有更新都会产生相同的 currentTime

MAGIC_NUMBER_OFFSET 的作用是避免计算的值和 NoWork 以及 Sync 相同,因为它俩的值分别为 01,所以 MAGIC_NUMBER_OFFSET 的值为 2。

了解了 currentTime 的计算过程,接下来看下 expirationTime 的计算过程:

computeExpirationForFiber

function computeExpirationForFiber(currentTime: ExpirationTime, fiber: Fiber) {
  let expirationTime;
  if (expirationContext !== NoWork) {
    expirationTime = expirationContext;
  } else if (isWorking) {
    if (isCommitting) {
      expirationTime = Sync;
    } else {
      expirationTime = nextRenderExpirationTime;
    }
  } else {
    if (fiber.mode & ConcurrentMode) {
      if (isBatchingInteractiveUpdates) {
        expirationTime = computeInteractiveExpiration(currentTime);
      } else {
        expirationTime = computeAsyncExpiration(currentTime);
      }
      if (nextRoot !== null && expirationTime === nextRenderExpirationTime) {
        expirationTime += 1;
      }
    } else {
      expirationTime = Sync;
    }
  }
  if (isBatchingInteractiveUpdates) {
    if (expirationTime > lowestPriorityPendingInteractiveExpirationTime) {
      lowestPriorityPendingInteractiveExpirationTime = expirationTime;
    }
  }
  return expirationTime;
}

expirationContext 保存了 expirationTime 上下文,在 syncUpdatesdeferredUpdates 中分别被设置为 Sync 和根据 currentTime 计算的 expirationTime,即 AsyncExpirationTime

isWorking 表示当前是否有更新正在进行,commitRootrenderRoot 开始时会置为 true,结束时置为 falseisCommitting 表示是否处于 commit 阶段,在 commitRoot 时设置状态。以上情况的 expirationTime 赋值到后面再说,现在先忽略,毕竟首次渲染的流程才刚开始。

渲染 root 节点的时候,传入的 isConcurrentfalse,所以 fiber.mode & ConcurrentMode 的值为 NoContext & ConcurrentMode,即 0b000 & 0b001 等于 0,所以计算出来的 expirationTime 值为 Sync,即最高优先级。isBatchingInteractiveUpdates 会在 interactiveUpdates 中 更新为 true,表示正处于事件类触发的更新。

computeAsyncExpiration & computeInteractiveExpiration

function ceiling(num: number, precision: number): number {
  return (((num / precision) | 0) + 1) * precision;
}

const UNIT_SIZE = 10;
const MAGIC_NUMBER_OFFSET = 2;

function expirationTimeToMs(expirationTime: ExpirationTime): number {
  return (expirationTime - MAGIC_NUMBER_OFFSET) * UNIT_SIZE;
}
function msToExpirationTime(ms: number): ExpirationTime {
  return ((ms / UNIT_SIZE) | 0) + MAGIC_NUMBER_OFFSET;
}

function computeExpirationBucket(
  currentTime,
  expirationInMs, // LOW_PRIORITY_EXPIRATION = 5000;  HIGH_PRIORITY_EXPIRATION = 150;
  bucketSizeMs, // LOW_PRIORITY_BATCH_SIZE = 250;  HIGH_PRIORITY_BATCH_SIZE = 100;
): ExpirationTime {
  return (
    MAGIC_NUMBER_OFFSET +
    ceiling(
      currentTime - MAGIC_NUMBER_OFFSET + expirationInMs / UNIT_SIZE,
      bucketSizeMs / UNIT_SIZE,
    )
  );
}

const LOW_PRIORITY_EXPIRATION = 5000;
const LOW_PRIORITY_BATCH_SIZE = 250;
function computeAsyncExpiration(
  currentTime: ExpirationTime,
): ExpirationTime {
  return computeExpirationBucket(
    currentTime,
    LOW_PRIORITY_EXPIRATION,
    LOW_PRIORITY_BATCH_SIZE,
  );
}

const HIGH_PRIORITY_EXPIRATION = 150;
const HIGH_PRIORITY_BATCH_SIZE = 100;

function computeInteractiveExpiration(currentTime: ExpirationTime) {
  return computeExpirationBucket(
    currentTime,
    HIGH_PRIORITY_EXPIRATION,
    HIGH_PRIORITY_BATCH_SIZE,
  );
}

ceiling 方法的作用是可以传入一个区间,使得两个在该区间的数,经过 ceiling 方法计算后,可以得到相同的值。

computeAsyncExpiration 为例,此时的区间为 bucketSizeMs / UNIT_SIZE250 / 1025,意思是两次更新的时间间隔若小于25ms,则计算出来的过期时间便是相同的,来达到批量更新的目的。计算 currentTime 的时候是除了一个 UNIT_SIZE 的,所以计算过程中都除了 UNIT_SIZE。计算 currentTime 的时候加了MAGIC_NUMBER_OFFSET,所以这里又减去了 MAGIC_NUMBER_OFFSET

根据上面的计算,低优先级和高优先级的时间差分别为 25ms10ms,以上复杂的计算过程主要目的还是为了能合并多次更新,使在同一时间间隔内的更新能够 batchUpdate

4.4 开始创建update

updateContainerAtExpirationTime

function updateContainerAtExpirationTime(
  element: ReactNodeList, // <App>
  container: OpaqueRoot, // FiberRoot
  parentComponent: ?React$Component<any, any>, // null
  expirationTime: ExpirationTime,
  callback: ?Function, 
) {
  const current = container.current; // RootFiber
  return scheduleRootUpdate(current, element, expirationTime, callback);
}

scheduleRootUpdate

function scheduleRootUpdate(
  current: Fiber, // RootFiber
  element: ReactNodeList, // <App>
  expirationTime: ExpirationTime,
  callback: ?Function,
) {
  const update = createUpdate(expirationTime);
  update.payload = {element};
  callback = callback === undefined ? null : callback;
  if (callback !== null) {
    update.callback = callback;
  }
  enqueueUpdate(current, update);
  scheduleWork(current, expirationTime);
  return expirationTime;
}

createUpdate

创建 update 对象

function createUpdate(expirationTime: ExpirationTime): Update<*> {
  return {
    expirationTime: expirationTime,
    tag: UpdateState, // UpdateState = 0
    payload: null,
    callback: null,
    next: null,
    nextEffect: null,
  };
}

tag 属性会有以下值:

const UpdateState = 0;
const ReplaceState = 1;
const ForceUpdate = 2;
const CaptureUpdate = 3;

enqueueUpdate

将创建好的 update 入队 updateQueue

function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
  // Update queues are created lazily.
  const alternate = fiber.alternate;
  let queue1;
  let queue2;
  if (alternate === null) {
    // There's only one fiber.
    queue1 = fiber.updateQueue;
    queue2 = null;
    if (queue1 === null) {
      queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
    }
  } else {
    // There are two owners.
    queue1 = fiber.updateQueue;
    queue2 = alternate.updateQueue;
    if (queue1 === null) {
      if (queue2 === null) {
        // Neither fiber has an update queue. Create new ones.
        queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
        queue2 = alternate.updateQueue = createUpdateQueue(
          alternate.memoizedState,
        );
      } else {
        // Only one fiber has an update queue. Clone to create a new one.
        queue1 = fiber.updateQueue = cloneUpdateQueue(queue2);
      }
    } else {
      if (queue2 === null) {
        // Only one fiber has an update queue. Clone to create a new one.
        queue2 = alternate.updateQueue = cloneUpdateQueue(queue1);
      } else {
        // Both owners have an update queue.
      }
    }
  }
  if (queue2 === null || queue1 === queue2) {
    // There's only a single queue.
    appendUpdateToQueue(queue1, update);
  } else {
    // There are two queues. We need to append the update to both queues,
    // while accounting for the persistent structure of the list — we don't
    // want the same update to be added multiple times.
    if (queue1.lastUpdate === null || queue2.lastUpdate === null) {
      // One of the queues is not empty. We must add the update to both queues.
      appendUpdateToQueue(queue1, update);
      appendUpdateToQueue(queue2, update);
    } else {
      // Both queues are non-empty. The last update is the same in both lists,
      // because of structural sharing. So, only append to one of the lists.
      appendUpdateToQueue(queue1, update);
      // But we still need to update the `lastUpdate` pointer of queue2.
      queue2.lastUpdate = update;
    }
  }
}

这里的判断逻辑比较复杂,所以可以先走下当前渲染根节点的判断逻辑。这里涉及到了 fiber 节点上的 alternate 属性,这个属性的作用是在更新某个节点的时候,会创建出当前 fiber 的克隆体,源码中叫做 workInProgress,更新会在这个属性上进行,而不会影响原来的 fiber 的状态,等渲染完成后,当前 fiber 会被更新好后的 workInProgress 替换掉,作用先大概知道下,接着往下看。第一次渲染 alternatequeue2fiber.updateQueue 均为 null,所以会调用 createUpdateQueue 方法创建 updateQueue

createUpdateQueue

创建 updateQueue

function createUpdateQueue<State>(baseState: State): UpdateQueue<State> {
  const queue: UpdateQueue<State> = {
    baseState,
    firstUpdate: null,
    lastUpdate: null,
    firstCapturedUpdate: null,
    lastCapturedUpdate: null,
    firstEffect: null,
    lastEffect: null,
    firstCapturedEffect: null,
    lastCapturedEffect: null,
  };
  return queue;
}

接下来会将新创建的 update 入队当前 fiber 节点上的 updateQueue

appendUpdateToQueue

function appendUpdateToQueue<State>(
  queue: UpdateQueue<State>,
  update: Update<State>,
) {
  // Append the update to the end of the list.
  if (queue.lastUpdate === null) {
    // Queue is empty
    queue.firstUpdate = queue.lastUpdate = update;
  } else {
    queue.lastUpdate.next = update;
    queue.lastUpdate = update;
  }
}

firstUpdate 记录第一个 updatelastUpdate 记录最后一个 update,每次新进入一个 update,就将 lastUpdatenext 属性赋值为新的 update,再将 lastUpdate 赋值为新的 update

1-4 阶段小结

  1. 调用 ReactDOM.render 开始根节点渲染。
  2. render 方法调用 legacyRenderSubtreeIntoContainer 生成 ReactRoot 类的实例,该类会为实例创建一个 _internalRoot 属性,即会创建一个 FiberRoot 对象,该对象上的 current 节点指向一个叫 RootFiberfiber 结构。fiber 对象会记录当前 react 节点的结构、stateprops、更新队列、effect、过期时间等信息。
  3. 创建完 FiberRoot 对象后,便会调用实例的 render 方法,实际是调用 updateContainer 创建更新。
  4. updateContainer 中,会为根节点生成过期时间 expirationTimeexpirationTime 表示节点的更新优先级,会有几个固定值,Sync 为最高优先级,值为 1,根节点的初始渲染的优先级为 Sync。其他的更新会根据是否为 Concurrent Mode 来计算 AsyncExpirationTime,计算 expirationTime 之前会计算一个叫 currentTime 的时间差,react 代码执行之初会保存一个类似 date.now 的时间节点,currentTime 则是用每次触发更新的 date.now 减去这个初始时间点,再抹平 10ms 的误差。接下来会用这个 currentTime 计算高优先级任务和低优先级任务的过期时间,低优先级和高优先级的时间差分别为 25ms 和 10ms,意思是在同一时间差中的更新,会认为是同一优先级的更新,从而进行更新合并。
  5. 计算完过期时间后,则会调用 scheduleRootUpdate 创建更新,首先会创建一个update对象,该对象记录了当前更新的 tagexpirationTimeeffect 等信息,然后为当前 fiber 节点创建 updateQueue,再将 update 入队 fiber 节点的 updateQueue 中,updateQueue 是一个链表结构,每次入队的 update 会被放到链表的最后一个节点。

从下一章开始,会出现大量的全局变量,这些全局变量对于 react 源码的运行方式有至关重要的作用,所以在每章结束后,我会总结一波供大家参考和提升记忆。

5. 调度 schedule

5.1 scheduleWork

当前渲染根节点,传入的 fiberRootFiber

function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
  const root = scheduleWorkToRoot(fiber, expirationTime);
  if (root === null) {
    return;
  }
  markPendingPriorityLevel(root, expirationTime);
  if (
    // If we're in the render phase, we don't need to schedule this root
    // for an update, because we'll do it before we exit...
    !isWorking ||
    isCommitting ||
    // ...unless this is a different root than the one we're rendering.
    nextRoot !== root
  ) {
    const rootExpirationTime = root.expirationTime;
    requestWork(root, rootExpirationTime);
  }
}

scheduleWorkToRoot

function scheduleWorkToRoot(fiber: Fiber, expirationTime): FiberRoot | null {
  // Update the source fiber's expiration time
  if (
    fiber.expirationTime === NoWork ||
    fiber.expirationTime > expirationTime
  ) {
    fiber.expirationTime = expirationTime;
  }
  let alternate = fiber.alternate;
  if (
    alternate !== null &&
    (alternate.expirationTime === NoWork ||
      alternate.expirationTime > expirationTime)
  ) {
    alternate.expirationTime = expirationTime;
  }
  // Walk the parent path to the root and update the child expiration time.
  let node = fiber.return;
  let root = null;
  if (node === null && fiber.tag === HostRoot) {
    root = fiber.stateNode;
  } else {
    while (node !== null) {
      alternate = node.alternate;
      if (
        node.childExpirationTime === NoWork ||
        node.childExpirationTime > expirationTime
      ) {
        node.childExpirationTime = expirationTime;
        if (
          alternate !== null &&
          (alternate.childExpirationTime === NoWork ||
            alternate.childExpirationTime > expirationTime)
        ) {
          alternate.childExpirationTime = expirationTime;
        }
      } else if (
        alternate !== null &&
        (alternate.childExpirationTime === NoWork ||
          alternate.childExpirationTime > expirationTime)
      ) {
        alternate.childExpirationTime = expirationTime;
      }
      if (node.return === null && node.tag === HostRoot) {
        root = node.stateNode;
        break;
      }
      node = node.return;
    }
  }
  if (root === null) {
    return null;
  }
  return root;
}

scheduleWorkToRoot 方法的主要作用为:1. 更新当前节点的 expirationTime,更新当前节点 alternateexpirationTime,若传入的 expirationTime 更小,即优先级更高,则更新。2. 从当前节点开始,向上遍历,更新父节点的 childExpirationTime,更新父节点的 alternateexpirationTime,若传入的 expirationTime 更小,即优先级更高,则更新。直至遍历到 RootFiber。3. 返回 RootFiberstateNode,即 FiberRoot

markPendingPriorityLevel

function markPendingPriorityLevel(
  root: FiberRoot,
  expirationTime: ExpirationTime,
): void {
  // If there's a gap between completing a failed root and retrying it,
  // additional updates may be scheduled. Clear `didError`, in case the update
  // is sufficient to fix the error.
  root.didError = false;

  // Update the latest and earliest pending times
  const earliestPendingTime = root.earliestPendingTime;
  if (earliestPendingTime === NoWork) {
    // No other pending updates.
    root.earliestPendingTime = root.latestPendingTime = expirationTime;
  } else {
    if (earliestPendingTime > expirationTime) {
      // This is the earliest pending update.
      root.earliestPendingTime = expirationTime;
    } else {
      const latestPendingTime = root.latestPendingTime;
      if (latestPendingTime < expirationTime) {
        // This is the latest pending update
        root.latestPendingTime = expirationTime;
      }
    }
  }
  findNextExpirationTimeToWorkOn(expirationTime, root);
}

markPendingPriorityLevel 方法的作用是更新 FiberRootearliestPendingTimelatestPendingTime 属性,这俩属性记录了 FiberRoot 上的最高和最低优先级的 expirationTime

findNextExpirationTimeToWorkOn

function findNextExpirationTimeToWorkOn(completedExpirationTime, root) {
  const earliestSuspendedTime = root.earliestSuspendedTime;
  const latestSuspendedTime = root.latestSuspendedTime;
  const earliestPendingTime = root.earliestPendingTime;
  const latestPingedTime = root.latestPingedTime;

  // Work on the earliest pending time. Failing that, work on the latest
  // pinged time.
  let nextExpirationTimeToWorkOn =
    earliestPendingTime !== NoWork ? earliestPendingTime : latestPingedTime;

  // If there is no pending or pinged work, check if there's suspended work
  // that's lower priority than what we just completed.
  if (
    nextExpirationTimeToWorkOn === NoWork &&
    (completedExpirationTime === NoWork ||
      latestSuspendedTime > completedExpirationTime)
  ) {
    // The lowest priority suspended work is the work most likely to be
    // committed next. Let's start rendering it again, so that if it times out,
    // it's ready to commit.
    nextExpirationTimeToWorkOn = latestSuspendedTime;
  }

  let expirationTime = nextExpirationTimeToWorkOn;
  if (
    expirationTime !== NoWork &&
    earliestSuspendedTime !== NoWork &&
    earliestSuspendedTime < expirationTime
  ) {
    // Expire using the earliest known expiration time.
    expirationTime = earliestSuspendedTime;
  }

  root.nextExpirationTimeToWorkOn = nextExpirationTimeToWorkOn;
  root.expirationTime = expirationTime;
}

findNextExpirationTimeToWorkOn 方法的作用是定义了 FiberRoot 节点上的 expirationTimenextExpirationTimeToWorkOn 属性,这两个属性大部分情况下是相同的。

5.2 requestWork

function requestWork(root: FiberRoot, expirationTime: ExpirationTime) {
  addRootToSchedule(root, expirationTime);
  if (isRendering) {
    // Prevent reentrancy. Remaining work will be scheduled at the end of
    // the currently rendering batch.
    return;
  }

  if (isBatchingUpdates) {
    // Flush work at the end of the batch.
    if (isUnbatchingUpdates) {
      // ...unless we're inside unbatchedUpdates, in which case we should
      // flush it now.
      nextFlushedRoot = root;
      nextFlushedExpirationTime = Sync;
      performWorkOnRoot(root, Sync, true);
    }
    return;
  }

  if (expirationTime === Sync) {
    performSyncWork();
  } else {
    scheduleCallbackWithExpirationTime(root, expirationTime);
  }
}

addRootToSchedule

function addRootToSchedule(root: FiberRoot, expirationTime: ExpirationTime) {
  // Add the root to the schedule.
  // Check if this root is already part of the schedule.
  if (root.nextScheduledRoot === null) {
    // This root is not already scheduled. Add it.
    root.expirationTime = expirationTime;
    if (lastScheduledRoot === null) {
      firstScheduledRoot = lastScheduledRoot = root;
      root.nextScheduledRoot = root;
    } else {
      lastScheduledRoot.nextScheduledRoot = root;
      lastScheduledRoot = root;
      lastScheduledRoot.nextScheduledRoot = firstScheduledRoot;
    }
  } else {
    // This root is already scheduled, but its priority may have increased.
    const remainingExpirationTime = root.expirationTime;
    if (
      remainingExpirationTime === NoWork ||
      expirationTime < remainingExpirationTime
    ) {
      // Update the priority.
      root.expirationTime = expirationTime;
    }
  }
}

react 可以创建多个 root 节点,所以这些 root 节点就会形成一个环状链表,addRootToSchedule 方法的作用就是将新创建的 FiberRoot 节点插入到这个链表中。这里 root.nextScheduledRootnull 表示这是一个新的 root 节点,lastScheduledRootnull 表示当前的链表是空的,所以这里会有创建链表和插入链表的逻辑。如果 root.nextScheduledRoot 不为 null,说明当前的的 root 节点已经被调度了,这里就会更新一下该 root 节点的优先级。

接下来我们再回过头看下 4.2 小节中的 findHighestPriorityRoot 方法:

findHighestPriorityRoot

function findHighestPriorityRoot() {
  let highestPriorityWork = NoWork;
  let highestPriorityRoot = null;
  if (lastScheduledRoot !== null) {
    let previousScheduledRoot = lastScheduledRoot;
    let root = firstScheduledRoot;
    while (root !== null) {
      const remainingExpirationTime = root.expirationTime;
      if (remainingExpirationTime === NoWork) {
        if (root === root.nextScheduledRoot) {
          // This is the only root in the list.
          root.nextScheduledRoot = null;
          firstScheduledRoot = lastScheduledRoot = null;
          break;
        } else if (root === firstScheduledRoot) {
          // This is the first root in the list.
          const next = root.nextScheduledRoot;
          firstScheduledRoot = next;
          lastScheduledRoot.nextScheduledRoot = next;
          root.nextScheduledRoot = null;
        } else if (root === lastScheduledRoot) {
          // This is the last root in the list.
          lastScheduledRoot = previousScheduledRoot;
          lastScheduledRoot.nextScheduledRoot = firstScheduledRoot;
          root.nextScheduledRoot = null;
          break;
        } else {
          previousScheduledRoot.nextScheduledRoot = root.nextScheduledRoot;
          root.nextScheduledRoot = null;
        }
        root = previousScheduledRoot.nextScheduledRoot;
      } else {
        if (
          highestPriorityWork === NoWork ||
          remainingExpirationTime < highestPriorityWork
        ) {
          // Update the priority, if it's higher
          highestPriorityWork = remainingExpirationTime;
          highestPriorityRoot = root;
        }
        if (root === lastScheduledRoot) {
          break;
        }
        if (highestPriorityWork === Sync) {
          // Sync is highest priority by definition so
          // we can stop searching.
          break;
        }
        previousScheduledRoot = root;
        root = root.nextScheduledRoot;
      }
    }
  }

  nextFlushedRoot = highestPriorityRoot;
  nextFlushedExpirationTime = highestPriorityWork;
}

此时 firstScheduledRootlastScheduledRoot 已经不为 null 了,这里会有一个 root 链表的循环遍历,从 firstScheduledRoot 链表头部开始,首先判断了当前 root 节点的优先级是否为 NoWork 来表示该 root 节点是否被调度完毕。当优先级为 NoWork 时,这里会有一个删除该节点的逻辑,相当于删除链表中的某个节点,就是说会删除调度完毕的 root 节点。如果当前 root 节点的优先级不为 NoWork,则会依次遍历整个链表,找到优先级最高的root节点和其优先级,然后赋值给全局变量 nextFlushedRootnextFlushedExpirationTime(这俩全局变量的使用后面会讲到),如果优先级为 Sync 就不用再遍历下去了,因为 Sync 是最高优先级。因为是环状链表,所以 root === lastScheduledRoot 成立时也要停止循环防止无限循环下去。

5.3 AsyncExpirationTime 优先级任务的调度

scheduleCallbackWithExpirationTime

因为 performAsyncWorkperformSyncWork 这两个方法最终都会调用 performWork 这个方法,为了能先了解 AsyncExpirationTime 优先级任务的调度方式,我们可以先看一下scheduleCallbackWithExpirationTime 方法的实现。

function scheduleCallbackWithExpirationTime(
  root: FiberRoot,
  expirationTime: ExpirationTime,
) {
  if (callbackExpirationTime !== NoWork) {
    // A callback is already scheduled. Check its expiration time (timeout).
    if (expirationTime > callbackExpirationTime) {
      // Existing callback has sufficient timeout. Exit.
      return;
    } else {
      if (callbackID !== null) {
        // Existing callback has insufficient timeout. Cancel and schedule a
        // new one.
        cancelDeferredCallback(callbackID);
      }
    }
    // The request callback timer is already running. Don't start a new one.
  } else {
    startRequestCallbackTimer();
  }

  callbackExpirationTime = expirationTime;
  const currentMs = now() - originalStartTimeMs;
  const expirationTimeMs = expirationTimeToMs(expirationTime);
  const timeout = expirationTimeMs - currentMs;
  callbackID = scheduleDeferredCallback(performAsyncWork, {timeout});
}

callbackExpirationTime 表示当前被调度的 callbackexpirationTime,如果有新的更新且比当前的任务优先级低的话,就 return,如果优先级更高,则取消当前正在执行的 callback 调用。这里不用担心说取消掉就没有了,因为所有进入的 callback 都是存在一个 callbackNodeList 链表上的,这个就是下面 scheduleDeferredCallback 方法要做的事情,被取消的 callback 只是在这个链表上改变了下顺序而已,新的优先级更高的 callback 放在了被取消的 callback 之前,最终还是会轮到它执行的。

currentMs 表示此刻距离 js 开始加载的时间差,expirationTimeToMs 方法相当于是对之前通过一系列计算得到的 expirationTime 进行了时间戳的还原,乘以了一个 UNIT_SIZEexpirationTimeToMs 方法可在 4.3 小节中找到。因为 expirationTimeMs 是一个基于 originalStartTimeMs 的未来的时间戳节点,所以减去 currentMs 后,表示当前时间距离过期时间的时间段大小。

scheduleDeferredCallback

scheduleDeferredCallback 方法的实现,当前 react 版本已经抽成了一个独立的包,代码在scheduler/src/Scheduler.js 目录下,即该文件下的 unstable_scheduleCallback 方法,cancelDeferredCallback 则对应为 unstable_cancelCallback 方法。

unstable_scheduleCallback

function unstable_scheduleCallback(callback, deprecated_options) {
  var startTime =
    currentEventStartTime !== -1 ? currentEventStartTime : getCurrentTime();

  var expirationTime;
  if (
    typeof deprecated_options === 'object' &&
    deprecated_options !== null &&
    typeof deprecated_options.timeout === 'number'
  ) {
    expirationTime = startTime + deprecated_options.timeout;
  }

  var newNode = {
    callback,
    priorityLevel: currentPriorityLevel,
    expirationTime,
    next: null,
    previous: null,
  };

  // Insert the new callback into the list, ordered first by expiration, then
  // by insertion. So the new callback is inserted any other callback with
  // equal expiration.
  if (firstCallbackNode === null) {
    // This is the first callback in the list.
    firstCallbackNode = newNode.next = newNode.previous = newNode;
    ensureHostCallbackIsScheduled();
  } else {
    var next = null;
    var node = firstCallbackNode;
    do {
      if (node.expirationTime > expirationTime) {
        // The new callback expires before this one.
        next = node;
        break;
      }
      node = node.next;
    } while (node !== firstCallbackNode);

    if (next === null) {
      // No callback with a later expiration was found, which means the new
      // callback has the latest expiration in the list.
      next = firstCallbackNode;
    } else if (next === firstCallbackNode) {
      // The new callback has the earliest expiration in the entire list.
      firstCallbackNode = newNode;
      ensureHostCallbackIsScheduled();
    }

    var previous = next.previous;
    previous.next = next.previous = newNode;
    newNode.next = next;
    newNode.previous = previous;
  }

  return newNode;
}

参数 callback 即为传入的 performAsyncWork,这里我们先关注调度的逻辑,认为就是传入了一个普通的回调函数。deprecated_options.timeout 表示一个过期时间。

startTime 可以认为是当前时间,重新计算的 expirationTime 值为当前时间加上传入的过期时间。之前比较疑惑为什么要重新生成一个过期时间,为什么不直接使用之前计算好的 expirationTime,后来才醒悟到这是一个独立的包,传入的参数就是一个过期时间而已,这也就理解了为什么 scheduleCallbackWithExpirationTime 方法中为何要大费周章的计算出一个 timeout,然后在这个方法里又加上一个 currentTime 了。

这里生成了一个 newNode 节点,是一个环状链表,会把多个 callback 根据优先级大小进行排序,firstCallbackNode 代表了优先级最高的一个 callback

firstCallbackNode 不为空时,会遍历这个链表,找到第一个比新进入的 callback 优先级低的任务,将其赋值给变量 next,表示下一个要执行的 callback,然后跳出循环。遍历完后,如果 nextnull,表示新的 callback 比之前的所有回调的优先级都低,则将 next 赋值为 firstCallbackNode,如果next 的值为 firstCallbackNode,表示新进入的 callback 的优先级是最高的,则把 firstCallbackNode 赋值为新的 callback 节点。

接下来则是双向链表的插入逻辑。定义的变量 previous 相当于是做了一次 next.previous 的副本,因为后面要改变 next.previous 的值,又要保留前一个节点。previous.next = next.previous = newNode 意思就是把新节点插入到 nextnext.previous 中间,相当于插到了 next 的前面。接下来就是设置newNodenextprevious 了。最后返回了 newNode

ensureHostCallbackIsScheduled

function ensureHostCallbackIsScheduled() {
  if (isExecutingCallback) {
    // Don't schedule work yet; wait until the next time we yield.
    return;
  }
  // Schedule the host callback using the earliest expiration in the list.
  var expirationTime = firstCallbackNode.expirationTime;
  if (!isHostCallbackScheduled) {
    isHostCallbackScheduled = true;
  } else {
    // Cancel the existing host callback.
    cancelHostCallback();
  }
  requestHostCallback(flushWork, expirationTime);
}

该方法主要在 firstCallbackNode 改变后会调用。

isExecutingCallback 会在 flushWork 方法中会置为 true,表示当前是否正在执行callback,执行完后置为 falseisHostCallbackScheduled 表示当前是否正在处于调度中,只有当 firstCallbackNodenull 时,才会置为 false,表示链表上callback 都处理完了,每次调度前都会置为 true

如果当前没有在执行回调且没有回调被调度,isHostCallbackScheduled 置为 true,否则取消当前的回调,取消是因为 firstCallbackNode 变化了。cancelHostCallback 方法是把 scheduledHostCallback 置为了 null,下面的 requestHostCallback 会重置新为新的 callback

接下来的 callback 会变成 flushWork,即 scheduledHostCallback链表上的 callback 是在 flushWork 中执行的,这个先不用着急去了解,后面会讲到。

requestHostCallback

function requestHostCallback = function(callback, absoluteTimeout) {
    scheduledHostCallback = callback;
    timeoutTime = absoluteTimeout;
    if (isFlushingHostCallback || absoluteTimeout < 0) {
      // Don't wait for the next frame. Continue working ASAP, in a new event.
      window.postMessage(messageKey, '*');
    } else if (!isAnimationFrameScheduled) {
      // If rAF didn't already schedule one, we need to schedule a frame.
      isAnimationFrameScheduled = true;
      requestAnimationFrameWithTimeout(animationTick);
    }
  };

这里可以先看下 else 的判断,isAnimationFrameScheduled 表示是否已经开始调用requestAnimationFrame

requestAnimationFrameWithTimeout

var ANIMATION_FRAME_TIMEOUT = 100;
var rAFID;
var rAFTimeoutID;
var requestAnimationFrameWithTimeout = function(callback) {  // callback animationTick
  // schedule rAF and also a setTimeout
  rAFID = localRequestAnimationFrame(function(timestamp) {
    // cancel the setTimeout
    localClearTimeout(rAFTimeoutID);
    callback(timestamp);
  });
  rAFTimeoutID = localSetTimeout(function() {
    // cancel the requestAnimationFrame
    localCancelAnimationFrame(rAFID);
    callback(getCurrentTime());
  }, ANIMATION_FRAME_TIMEOUT);
};

这里前缀为有 local 的方法可以认为是去掉前缀 local 的原生方法,react 这里做了一层兜底。这里有一个竞争关系,就是 100ms 之后,如果 requestAnimationFrame 还没有执行,那就直接取消 requestAnimationFrame,然后立即执行 callback,要是先执行了 requestAnimationFrame,则取消定时器。timestamprequestAnimationFrame 传给回调函数的一个时间戳,表示回调函数执行的时间。

animationTick

var animationTick = function (rafTime) {
    if (scheduledHostCallback !== null) {
      requestAnimationFrameWithTimeout(animationTick);
    } else {
      isAnimationFrameScheduled = false;
      return;
    }
    var nextFrameTime = rafTime - frameDeadline + activeFrameTime;
    if (
      nextFrameTime < activeFrameTime &&
      previousFrameTime < activeFrameTime
    ) {
      if (nextFrameTime < 8) {
        nextFrameTime = 8;
      }
      activeFrameTime =
        nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime;
    } else {
      previousFrameTime = nextFrameTime;
    }
    frameDeadline = rafTime + activeFrameTime;
    if (!isMessageEventScheduled) {
      isMessageEventScheduled = true;
      window.postMessage(messageKey, '*');
    }
 };
  window.addEventListener('message', idleTick, false);

该方法的作用主要是动态调整刷新频率及计算当前帧的到期时间。

scheduledHostCallbackflushWork,如果不为空,则再次调用 requestAnimationFrame,因为本身还是需要再次进入下一帧的。

rafTimerequestAnimationFrame 的回调函数的参数,表示 callback 被触发的时间,是一个时间戳,单位为 msframeDeadline 初始值为0,activeFrameTime 初始值为 33,即表示当前设备的屏幕刷新频率为 30帧/秒。初始的时候 nextFrameTime 会比较大,所以不会走第一个判断,else 里会给previousFrameTime 赋值为 nextFrameTime,这里表示的是两个相邻的帧时间。最后 frameDeadline表示到下一帧开始调用 callback 的时间点。

我们再看下 if 里的逻辑,第二次调用了 requestAnimationFrame 后,此时 frameDeadline 的值已经不为 0 了,所以 nextFrameTime 的值为 rafTime(下一帧开始调用 callback 的时间点) - rafTime(当前帧开始调用 callback 的时间点),这里的 activeFrameTime 因为还没有被重新赋值,所以第一次和第二次的值是相同的,所以就抵消了。此时 nextFrameTime 相当于就是设备真正的每一帧的时间间隔了。但还是不会走if的判断,因为 previousFrameTime 还是比 activeFrameTime 大。

第三次后,假设当前浏览器的刷新频率为 60帧/秒,则每一帧的时间是 16.7ms,此时 nextFrameTime 值为 16.7ms,是小于 activeFrameTime(33)成立,previousFrameTime 也是 16.7ms小于 activeFrameTime(33)成立。进入 if 后,发现 react 是不支持大于 120帧/秒 的刷新频率的,所以兜底了每一帧最小是 8ms,每次都会根据连续两次计算的帧时间动态赋值 activeFrameTime,目的是为了保证在高频率刷新情况下,来动态调整帧时间防止丢帧现象,取最大值是为了防止其中一个 callback 可能会错过当前帧的最后期限,那么就取这两个连续帧间隔的最大值。上面定义的全局变量 frameDeadline 后面会用到,表示当前帧的 deadline,结合 currentTime 会判断当前帧是否还有剩余时间。

最后会调用 postMessage 发起一个宏任务,window 监听到 message 事件后,会触发 idleTick 方法。

这里要解释下 react 为什么要使用 MessageChannel 来触发更新:

  1. 在一帧中,页面上的手势、用户交互、动画等行为和 js 代码执行是同步执行的,所以如果 reactjs 在一帧中执行的时间过长,并在下一帧中还在执行,这样会导致用户交互或者动画等行为不能在下一帧中执行,这必然会引起用户视觉上的卡顿,类似丢帧了。
  2. 为了解决这个问题,react 想在一帧中的空闲时间中去执行代码,在一帧的开始前先把主动权交给浏览器,这样用户交互或者动画等行为会先执行,然后 reactjs 代码就可以放心执行了,所以便引出了 requestIdleCallback 方法。
  3. 但为啥 react 没有直接使用 requestIdleCallback 方法呢,原因一是该方法存在兼容性问题,二是该方法只支持一帧 20ms 的刷新频率,对于主流一帧 16.7ms 的刷新频率来说,必然会造成丢帧效果,所以 react 使用了 requestAnimationFram + 宏任务 来模拟 requestIdleCallback
  4. 那为啥不直接使用 requestAnimationFram 呢,原因是因为 requestAnimationFram 是在重排和重绘之前执行的,这仍然没有把主动权交还给浏览器。
  5. 为啥不用微任务呢,微任务也是在dom渲染前执行的。
  6. 宏任务中为何不使用 setTimeout,因为 setTimeout 是有一个最小的时间阈值,一般是 4ms,所以使用了 MessageChannel 来执行宏任务,来模拟 setTimeout(fn, 0),而且还没有时延。

idleTick

var idleTick = function (event) {
    if (event.source !== window || event.data !== messageKey) {
      return;
    }
    isMessageEventScheduled = false;
    var prevScheduledCallback = scheduledHostCallback;
    var prevTimeoutTime = timeoutTime;
    scheduledHostCallback = null;
    timeoutTime = -1;
    var currentTime = getCurrentTime();
    var didTimeout = false;
    if (frameDeadline - currentTime <= 0) {
      // There's no time left in this idle period. Check if the callback has
      // a timeout and whether it's been exceeded.
      if (prevTimeoutTime !== -1 && prevTimeoutTime <= currentTime) {
        // Exceeded the timeout. Invoke the callback even though there's no
        // time left.
        didTimeout = true;
      } else {
        // No timeout.
        if (!isAnimationFrameScheduled) {
          // Schedule another animation callback so we retry later.
          isAnimationFrameScheduled = true;
          requestAnimationFrameWithTimeout(animationTick);
        }
        // Exit without invoking the callback.
        scheduledHostCallback = prevScheduledCallback;
        timeoutTime = prevTimeoutTime;
        return;
      }
    }
    if (prevScheduledCallback !== null) {
      isFlushingHostCallback = true;
      try {
        prevScheduledCallback(didTimeout);
      } finally {
        isFlushingHostCallback = false;
      }
    }
  };

isMessageEventScheduled 表示是否已经发起了 message 消息,因为已经接收到消息了,所以重置为了 falseframeDeadline 表示当前帧的过期时间,如果减去当前时间是小于 0 的,则表示本帧已经没有空闲时间去执行了,接下来又判断了当前 callback 过期时间是否小于当前时间,成立的话就表示回调已经过期,则需要立即执行,然后把 didTimeout 置为了 true,表示要执行的 callback 是已经过期的。若还没过期,则再次调用 requestAnimationFrame 在下一帧中调用。

最后执行了回调函数,即执行了 flushWork 方法,这里的全局变量 isFlushingHostCallback 意思就是 flushWork 方法是否正在执行,执行完后置为 false。这里执行的 callback 一类是因为当前帧已经没有剩余时间但本身的过期时间已经到了,不得不立即执行的 callback,另一类就是当前帧还有剩余时间且本身的过期时间还没有到的 callback

flushWork

function flushWork(didTimeout) {
  isExecutingCallback = true;
  deadlineObject.didTimeout = didTimeout;
  try {
    if (didTimeout) {
      // Flush all the expired callbacks without yielding.
      while (firstCallbackNode !== null) {
        // Read the current time. Flush all the callbacks that expire at or
        // earlier than that time. Then read the current time again and repeat.
        // This optimizes for as few performance.now calls as possible.
        var currentTime = getCurrentTime();
        if (firstCallbackNode.expirationTime <= currentTime) {
          do {
            flushFirstCallback();
          } while (
            firstCallbackNode !== null &&
            firstCallbackNode.expirationTime <= currentTime
          );
          continue;
        }
        break;
      }
    } else {
      // Keep flushing callbacks until we run out of time in the frame.
      if (firstCallbackNode !== null) {
        do {
          flushFirstCallback();
        } while (
          firstCallbackNode !== null &&
          getFrameDeadline() - getCurrentTime() > 0
        );
      }
    }
  } finally {
    isExecutingCallback = false;
    if (firstCallbackNode !== null) {
      // There's still work remaining. Request another callback.
      ensureHostCallbackIsScheduled();
    } else {
      isHostCallbackScheduled = false;
    }
    // Before exiting, flush all the immediate work that was scheduled.
    flushImmediateWork();
  }
}

isExecutingCallback 表示 callbackNodeList 节点上的 callbackperformAsyncWork 是否正在执行,等执行完后置为 false

开始执行 callbackNodeList 节点上的 callback 之前,会先判断 firstCallbackNode 节点上的callback 是否过期,若过期则从 firstCallbackNode 开始遍历链表,挨个执行节点上的expirationTime 小于当前时间节点的 callback,即过期的 callback,直到执行到未过期的 callback 节点,跳出循环,并重置 firstCallbackNode 为剩下的第一个未过期的节点。若未过期,则还是从 firstCallbackNode 开始遍历链表,直到当前帧已经没有空闲时间为止。前面的这些执行,都会将已经过期的 callbak 全部执行,或者在这一帧中尽可能多的执行 callback

当前帧结束前,如果 firstCallbackNode 仍不为 null,则说明还有未执行的 callback,则继续调用ensureHostCallbackIsScheduled 方法发起新的一帧的调用。如果为空了,表示所有的 callback 都已经执行完了,那就将 isHostCallbackScheduled 置为 false,表示本轮的调度结束了。

flushImmediateWork 会执行 callbackNodeList 节点上的 priorityLevel 属性为ImmediatePrioritycallback,当前版本 priorityLevel 的值应该都是 NormalPriority的,所以可以先忽略。

5.4 performWork

结束了 react 调度的逻辑,我们再回到 requestWork 方法中。

performSyncWork

该方法是在计算的 expirationTime 值为 Sync 时调用的,表示同步需要立即执行的更新。

function performSyncWork() {
  performWork(Sync, null);
}

方法很简单,就是调用了 performWork 方法。

performAsyncWork

该方法是在 scheduleCallbackWithExpirationTime 方法中调用的,表示异步更新,也就是在 scheduleDeferredCallback 方法中创建的 callbackNodeList 上的 callback 属性,真正的调用则是在 flushWork 中,前文中也是提过的。

function performAsyncWork(dl) {
  if (dl.didTimeout) {
    // The callback timed out. That means at least one update has expired.
    // Iterate through the root schedule. If they contain expired work, set
    // the next render expiration time to the current time. This has the effect
    // of flushing all expired work in a single batch, instead of flushing each
    // level one at a time.
    if (firstScheduledRoot !== null) {
      recomputeCurrentRendererTime();
      let root: FiberRoot = firstScheduledRoot;
      do {
        didExpireAtExpirationTime(root, currentRendererTime);
        // The root schedule is circular, so this is never null.
        root = (root.nextScheduledRoot: any);
      } while (root !== firstScheduledRoot);
    }
  }
  performWork(NoWork, dl);
}

参数 dl 就是 Schedule 包中的全局变量 deadlineObject,该对象的 didTimeout 属性表示当前的更新是否过期。这里又调用了一次 recomputeCurrentRendererTime 方法(在 4.3 节首次提到),主要是用来更新全局变量 currentRendererTime,这个全局变量就是计算 expirationTime 的时候用到的那个 currentTime。这里的循环更新了一遍所有 root 节点的 nextExpirationTimeToWorkOn 属性。

didExpireAtExpirationTime

function didExpireAtExpirationTime(
  root: FiberRoot,
  currentTime: ExpirationTime,
): void {
  const expirationTime = root.expirationTime;
  if (expirationTime !== NoWork && currentTime >= expirationTime) {
    // The root has expired. Flush all work up to the current time.
    root.nextExpirationTimeToWorkOn = currentTime;
  }
}

这里判断了下当前 root 节点的过期时间如果小于当前时间的话,表示当前 root 节点已经过期,则把该 root 节点的 nextExpirationTimeToWorkOn 赋值为当前时间,此时过期的 root 是需要马上执行更新的,所以 nextExpirationTimeToWorkOn 属性的作用猜测应该是记录了当前 root 节点将要更新的时间,这个属性将在 renderRoot 方法中用到。

performWork

function performWork(minExpirationTime: ExpirationTime, dl: Deadline | null) {
  deadline = dl;
  // Keep working on roots until there's no more work, or until we reach
  // the deadline.
  findHighestPriorityRoot();

  if (deadline !== null) {
    recomputeCurrentRendererTime();
    currentSchedulerTime = currentRendererTime;
    while (
      nextFlushedRoot !== null &&
      nextFlushedExpirationTime !== NoWork &&
      (minExpirationTime === NoWork ||
        minExpirationTime >= nextFlushedExpirationTime) &&
      (!deadlineDidExpire || currentRendererTime >= nextFlushedExpirationTime)
    ) {
      performWorkOnRoot(
        nextFlushedRoot,
        nextFlushedExpirationTime,
        currentRendererTime >= nextFlushedExpirationTime,
      );
      findHighestPriorityRoot();
      recomputeCurrentRendererTime();
      currentSchedulerTime = currentRendererTime;
    }
  } else {
    while (
      nextFlushedRoot !== null &&
      nextFlushedExpirationTime !== NoWork &&
      (minExpirationTime === NoWork ||
        minExpirationTime >= nextFlushedExpirationTime)
    ) {
      performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime, true);
      findHighestPriorityRoot();
    }
  }
  // We're done flushing work. Either we ran out of time in this callback,
  // or there's no more work left with sufficient priority.

  // If we're inside a callback, set this to false since we just completed it.
  if (deadline !== null) {
    callbackExpirationTime = NoWork;
    callbackID = null;
  }
  // If there's work left over, schedule a new callback.
  if (nextFlushedExpirationTime !== NoWork) {
    scheduleCallbackWithExpirationTime(
      ((nextFlushedRoot: any): FiberRoot),
      nextFlushedExpirationTime,
    );
  }

  // Clean-up.
  deadline = null;
  deadlineDidExpire = false;

  finishRendering();
}

这里首先调用了一次 findHighestPriorityRoot 是为了更新 nextFlushedRootnextFlushedExpirationTime 这两个全局变量,代表当前优先级最高的 root 节点和其过期时间。首先这里有一个判断 deadline 是否为 null,如果不为 null 则说明是被 performAsyncWork 方法调用的,即是一个异步更新,此时参数 minExpirationTimeNoWork,然后这里调用了 recomputeCurrentRendererTime 方法更新了全局变量 currentRendererTime,这里有个循环逻辑判断,全局变量 deadlineDidExpire 表示当前是否有还有正在执行的已过期的更新,performWorkOnRoot 中会置为 true,因为当前循环就是为了执行完所有已过期的更新任务的,所以跳出循环后会置为 falsecurrentRendererTime >= nextFlushedExpirationTime 意思是当前 root 节点是否过期。循环中执行了 performWorkOnRoot 方法(接下来会讲到),然后再次调用了 findHighestPriorityRoot(1) 和 recomputeCurrentRendererTime(2),然后更新了 currentSchedulerTime(3),大家可能会发现这三步好像都是一起执行的,在之前 4.3 小节中,计算 currentTime 的时候,也出现过类似的场景:

function requestCurrentTime() {
  if (isRendering) {
    return currentSchedulerTime;
  }
  findHighestPriorityRoot(); // 1
  if (
    nextFlushedExpirationTime === NoWork ||
    nextFlushedExpirationTime === Never
  ) {
    recomputeCurrentRendererTime();  // 2
    currentSchedulerTime = currentRendererTime; // 3
    return currentSchedulerTime;
  }
  return currentSchedulerTime;
}

所以这里可以看出当某个 root 节点执行完更新后,其 expirationTime 被重置为 NoWork 后(performWorkOnRoot 方法调用了 completeRoot 方法会将 expirationTime 属性重置为 NoWork),就会先调用 findHighestPriorityRoot 方法清除 expirationTimeNoWorkroot 节点,重置全局变量 nextFlushedRootnextFlushedExpirationTime,然后调用 recomputeCurrentRendererTime 更新全局变量 currentRendererTime,然后接着更新全局变量 currentSchedulerTime

接下来如果 deadline 值为 null,说明是被 performSyncWork 方法调用的,这里 minExpirationTimeSync 最高优先级,因为此时是同步更新任务,所以 nextFlushedExpirationTime 肯定为 Sync,这里 minExpirationTime >= nextFlushedExpirationTime 是成立的,循环中会执行完所有的同步任务然后跳出循环。

因为所有过期任务已经执行完了,所以清空了 callbackExpirationTimecallbackID

经过上面的循环,会有两种结果:一种是没有可更新的 root 节点了,此时 nextFlushedExpirationTimeNoWork,另一种是剩下了一些没有过期的且当前帧已经被耗尽了的任务,此时 nextFlushedExpirationTime 还不为 NoWork,则再次调用 scheduleCallbackWithExpirationTime 请求在下一帧中执行剩下的更新任务,这样经过一个大的循环后,最后会执行完所有 root 节点的更新。

finishRendering 方法后面再讲。

performWorkOnRoot

该方法的作用是调用 renderRoot 方法更新 FiberRoot 上的状态以及调用 completeRoot 完成对 fiber 树到 dom 树的映射。

function performWorkOnRoot(
  root: FiberRoot,
  expirationTime: ExpirationTime,
  isExpired: boolean,
) {
  isRendering = true;
  // Check if this is async work or sync/expired work.
  if (deadline === null || isExpired) {
    // Flush work without yielding.
    let finishedWork = root.finishedWork;
    if (finishedWork !== null) {
      // This root is already complete. We can commit it.
      completeRoot(root, finishedWork, expirationTime);
    } else {
      root.finishedWork = null;
      const isYieldy = false;
      renderRoot(root, isYieldy, isExpired);
      finishedWork = root.finishedWork;
      if (finishedWork !== null) {
        // We've completed the root. Commit it.
        completeRoot(root, finishedWork, expirationTime);
      }
    }
  } else {
    // Flush async work.
    let finishedWork = root.finishedWork;
    if (finishedWork !== null) {
      // This root is already complete. We can commit it.
      completeRoot(root, finishedWork, expirationTime);
    } else {
      root.finishedWork = null;
      const isYieldy = true;
      renderRoot(root, isYieldy, isExpired);
      finishedWork = root.finishedWork;
      if (finishedWork !== null) {
        // We've completed the root. Check the deadline one more time
        // before committing.
        if (!shouldYield()) {
          // Still time left. Commit the root.
          completeRoot(root, finishedWork, expirationTime);
        } else {
          // There's no time left. Mark this root as complete. We'll come
          // back and commit it later.
          root.finishedWork = finishedWork;
        }
      }
    }
  }
  isRendering = false;
}

进入该方法后会将全局变量 isRendering 置为 true,表示当前正在进行更新,等完成 renderRoot 或者 completeRoot 方法的调用后会置为 false

root 上的 finishedWork 属性是在 renderRoot 方法中对当前 FiberRoot 上的 current 属性,即 RootFiber 的一个拷贝,叫做 WorkInProgressRootFiber 上的状态更新都会先在这个备份的 fiber 上先做改变,完成后会赋值到 RootFiberalternate 属性上,然后用 alternate 属性渲染到 dom 上后,就会用 alternate 替换原来的 current 来完成 FiberRoot 的更新。

这里有两种情况的判断,if 中代表的是同步任务或已过期的任务(同步任务 deadlinenull),而且这些任务都是不可被中断的,这里判断了 root 上的 finishedWork 是否为空,如果不为空说明了某个异步任务上一帧中 renderRoot 已经调过了,但因为上一帧的时间被耗尽了,所以没有执行 completeRoot, 所以在这一帧中执行了下 completeRoot。如果为空,就顺序执行这两个方法。

else 里的就是异步任务,逻辑和 if 里的差不多,区别就是异步任务可以被中断,shouldYield 方法用来判断当前帧是否还有剩余时间,如果有继续执行 completeRoot,没有则把 finishedWork 赋值到 rootfinishedWork 属性上,这也就是 if 中判断 finishedWork 是否为空的原因了。

shouldYield

const timeHeuristicForUnitOfWork = 1;
function shouldYield() {
  if (deadlineDidExpire) {
    return true;
  }
  if (
    deadline === null ||
    deadline.timeRemaining() > timeHeuristicForUnitOfWork
  ) {
    // Disregard deadline.didTimeout. Only expired work should be flushed
    // during a timeout. This path is only hit for non-expired work.
    return false;
  }
  deadlineDidExpire = true;
  return true;
}
const timeRemaining = function () {
  // Fallback to Date.now()
  if (
    firstCallbackNode !== null &&
    firstCallbackNode.expirationTime < currentExpirationTime
  ) {
    return 0;
  }
  var remaining = getFrameDeadline() - Date.now();
  return remaining > 0 ? remaining : 0;
};
var deadlineObject = {
  timeRemaining,
  didTimeout: false,
};

这里就比较好理解了,调用 deadline 对象上的 timeRemaining 方法来判断的当前时间是否大于过期时间,大于说明当前帧已经没有剩余时间了。

renderRoot

这个方法主要是对不同类型的 React 组件的 fiber 树更新,以及处理各种错误,包括 16 版本中的新特性 error boundary 的处理逻辑,这里大部分代码会在下一章节中讲解。

function renderRoot(
  root: FiberRoot,
  isYieldy: boolean,
  isExpired: boolean,
): void {
  isWorking = true;
  ReactCurrentOwner.currentDispatcher = Dispatcher;
  const expirationTime = root.nextExpirationTimeToWorkOn;
  // Check if we're starting from a fresh stack, or if we're resuming from
  // previously yielded work.
  if (
    expirationTime !== nextRenderExpirationTime ||
    root !== nextRoot ||
    nextUnitOfWork === null
  ) {
    // Reset the stack and start working from the root.
    resetStack();
    nextRoot = root;
    nextRenderExpirationTime = expirationTime;
    nextUnitOfWork = createWorkInProgress(
      nextRoot.current,
      null,
      nextRenderExpirationTime,
    );
    root.pendingCommitExpirationTime = NoWork;
  }

  let didFatal = false;
  do {
    try {
      workLoop(isYieldy);
    } catch (thrownValue) {
      if (nextUnitOfWork === null) {
        // This is a fatal error.
        didFatal = true;
        onUncaughtError(thrownValue);
      } else {
        const failedUnitOfWork: Fiber = nextUnitOfWork;
        const sourceFiber: Fiber = nextUnitOfWork;
        let returnFiber = sourceFiber.return;
        if (returnFiber === null) {
          // This is the root. The root could capture its own errors. However,
          // we don't know if it errors before or after we pushed the host
          // context. This information is needed to avoid a stack mismatch.
          // Because we're not sure, treat this as a fatal error. We could track
          // which phase it fails in, but doesn't seem worth it. At least
          // for now.
          didFatal = true;
          onUncaughtError(thrownValue);
        } else {
          throwException(
            root,
            returnFiber,
            sourceFiber,
            thrownValue,
            nextRenderExpirationTime,
          );
          nextUnitOfWork = completeUnitOfWork(sourceFiber);
          continue;
        }
      }
    }
    break;
  } while (true);
  // We're done performing work. Time to clean up.
  isWorking = false;
  ReactCurrentOwner.currentDispatcher = null;
  resetContextDependences();
  // Yield back to main thread.
  if (didFatal) {
    ...
    return;
  }

  if (nextUnitOfWork !== null) {
    ...
    return;
  }

  // `nextRoot` points to the in-progress root. A non-null value indicates
  // that we're in the middle of an async render. Set it to null to indicate
  // there's no more work to be done in the current batch.
  nextRoot = null;
  interruptedBy = null;

  if (nextRenderDidError) {
    // There was an error
    ...
    return;
  }
  // Ready to commit.
  onComplete(root, rootWorkInProgress, expirationTime);
}

这里就可以看到调用了 createWorkInProgress 创建了 current 的拷贝,然后赋值给了全局变量 nextUnitOfWork。下面调用了 workLoop 方法来完成 workInProgress 的更新。

workLoop

function workLoop(isYieldy) {
  if (!isYieldy) {
    // Flush work without yielding
    while (nextUnitOfWork !== null) {
      nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    }
  } else {
    // Flush asynchronous work until the deadline runs out of time.
    while (nextUnitOfWork !== null && !shouldYield()) {
      nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    }
  }
}

这里的全局变量 nextUnitOfWork 在循环的开始的值就是当前节点对应的 fiber 树的拷贝,performUnitOfWork 方法会遍历完成当前 fiber 的所有子节点的更新,然后返回他的 return 属性,也就是他的父节点,直到 nextUnitOfWork 值为 null,表示已经回溯到根 fiber 了,即 RootFiber,因为只有 RootFiberreturn 属性值为 null

这里也是对可中断和不可中断的任务做了区分判断,对于可中断的异步任务做了是否有剩余时间的判断。

performUnitOfWork

function performUnitOfWork(workInProgress: Fiber): Fiber | null {
  const current = workInProgress.alternate;
  let next = beginWork(current, workInProgress, nextRenderExpirationTime);
  workInProgress.memoizedProps = workInProgress.pendingProps;
  if (next === null) {
    // If this doesn't spawn new work, complete the current work.
    next = completeUnitOfWork(workInProgress);
  }
  ReactCurrentOwner.current = null;
  return next;
}

beginWork 会遍历完当前 fiber 节点的所有子节点,返回 null 说明已经遍历完成了,则会调用 completeUnitOfWork 方法返回当前 fiberreturn 属性,即其父节点。这样的一个循环最终 performUnitOfWork 会返回null,表示已经更新完整个 RootFiber 树的状态了。

到此 React 调度的逻辑大概就结束了,更新相关的逻辑会在下一章中详细说明。

5 节中的全局变量整理

  1. isRendering:表示当前是否有节点正在更新中,包括 fiber 树的更新和从 fiber 树到 dom 树的渲染,在 performWorkOnRoot 前置为 true,结束后置为 false
  2. firstScheduledRoot:表示 FiberRootNodeList 链表中第一个 FiberRoot 节点。
  3. lastScheduledRoot:表示 FiberRootNodeList 链表中最后一个 FiberRoot 节点,与 firstScheduledRoot 会形成一个双向链表,这里的 FiberRoot 节点无优先级的排序,。
  4. nextFlushedRoot:在 findHighestPriorityRoot 中赋值,表示在 FiberRootNodeList 链表中找到的优先级最高的 FiberRoot 节点。
  5. nextFlushedExpirationTime:优先级最高的 FiberRoot 节点的过期时间,与 nextFlushedRoot 主要在 performWork 方法中使用。
  6. currentRendererTime:每次调用 performAsyncWork 前计算的 currentTime
  7. currentSchedulerTime:当某个 root 节点更新完后,再赋值为重新计算的 currentRendererTime

schedule 包中的全局变量:

  1. callbackExpirationTimeschedule 包中的全局变量,表示新进入的 callback 的过期时间。
  2. callbackIDschedule 包中的全局变量,表示新进入的 callback 的节点,所有进入的 callback 会形成一个环状链表。
  3. firstCallbackNodecallback 链表中的第一个 callbackNode,优先级是最高的。
  4. isExecutingCallback:表示当前是否有 callback 正在被执行,在 flushWork 中被赋值,callback 执行前置为 true,执行结束后置为 false
  5. isHostCallbackScheduled:表示当时是否有 callback 正被调度,在ensureHostCallbackIsScheduled 方法中置为 true,待 firstCallbackNode 为空后置为 false,表示一次调度已完成,callback 链表已经空了。
  6. scheduledHostCallback:这个 callback 指的是 flushWorkflushWork 中会执行 firstCallbackNode 上的 callback 属性,即传入的 performAsyncWork
  7. timeoutTimefirstCallbackNode 的过期时间。
  8. activeFrameTime:动态计算的一帧的时间。
  9. frameDeadline:当前帧的结束时间。
  10. isMessageEventScheduled:发送 message 之前置为 true,message的回调中置为 false,表示是否正在发送 message 消息。
  11. isAnimationFrameScheduledrequestAnimationFrame 执行前置为 true,当 scheduledHostCallbacknull 时,停止调用 requestAnimationFrame,置为 false。
  12. isFlushingHostCallback:表示 flushWork 方法是否正在执行,在 flushWork 方法执行前置为 true,等当前帧的时间耗尽后,flushWork也就结束调用了, 会被置为 false
  13. deadlineObject:表示当前 firstCallbackNode 的过期信息,其 didTimeout 属性表示是否过期,其 timeRemaining 方法可以实时获取当前帧的剩余时间。

6.未完待续。。。