# React是如何调度任务的

首先我们要知道哪些操作会让React开始一次任务调度

  1. ReactDOM.render或者ReactDOM.hydrate
  2. this.setState
  3. this.forceUpdate
  4. suspend component promise resolve or reject

那么从这几个方法入手,我们来看一下

# ReactDOM.render

首先会创建ReactRoot对象,然后调用他的render方法

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

其中DOMRendererreact-reconciler/src/ReactFiberReconciler,他的updateContainer如下在这里计算了一个时间,这个时间叫做expirationTime,顾名思义就是这次更新的 超时时间

关于时间是如何计算的看这里

然后调用了updateContainerAtExpirationTime,在这个方法里调用了scheduleRootUpdate就非常重要了

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

export function updateContainerAtExpirationTime(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  expirationTime: ExpirationTime,
  callback: ?Function,
) {
  // TODO: If this is a nested container, this won't be the root.
  const current = container.current;
  const context = getContextForSubtree(parentComponent);
  if (container.context === null) {
    container.context = context;
  } else {
    container.pendingContext = context;
  }

  return scheduleRootUpdate(current, element, expirationTime, callback);
}

# 开始调度

首先要生成一个update,不管你是setState还是ReactDOM.render造成的React更新,都会生成一个叫update的对象,并且会赋值给Fiber.updateQueue

关于update看这里

然后就是调用scheduleWork。注意到这里之前setStateReactDOM.render是不一样,但进入schedulerWork之后,就是任务调度的事情了,跟之前你是怎么调用的没有任何关系

function scheduleRootUpdate(
  current: Fiber,
  element: ReactNodeList,
  expirationTime: ExpirationTime,
  callback: ?Function,
) {
  const update = createUpdate(expirationTime);

  update.payload = {element};

  callback = callback === undefined ? null : callback;
  if (callback !== null) {
    warningWithoutStack(
      typeof callback === 'function',
      'render(...): Expected the last optional `callback` argument to be a ' +
        'function. Instead received: %s.',
      callback,
    );
    update.callback = callback;
  }
  enqueueUpdate(current, update);

  scheduleWork(current, expirationTime);
  return expirationTime;
}

# scheduleWork

这里先scheduleWorkToRoot,这一步非常重要,他主要做了一下几个任务

  • 找到当前Fiber的root
  • 给更新节点的父节点链上的每个节点的expirationTime设置为这个updateexpirationTime,除非他本身时间要小于expirationTime
  • 给更新节点的父节点链上的每个节点的childExpirationTime设置为这个updateexpirationTime,除非他本身时间要小于expirationTime

最终返回root节点的Fiber对象

然后进入一个判断:!isWorking && nextRenderExpirationTime !== NoWork && expirationTime < nextRenderExpirationTime,我们来解释一下这几个变量的意思

  1. isWorking代表是否正在工作,在开始renderRootcommitRoot的时候会设置为true,也就是在rendercommit两个阶段都会为true
  2. nextRenderExpirationTime在是新的renderRoot的时候会被设置为当前任务的expirationTime,而且一旦他被,只有当下次任务是NoWork的时候他才会被再次设置为NoWork,当然最开始也是NoWork

那么这个条件就很明显了:目前没有任何任务在执行,并且之前有执行过任务,同时当前的任务比之前执行的任务过期时间要早(也就是优先级要高)

那么这种情况会出现在什么时候呢?答案就是:上一个任务是异步任务(优先级很低,超时时间是502ms),并且在上一个时间片(初始是33ms)任务没有执行完,而且等待下一次requestIdleCallback的时候新的任务进来了,并且超时时间很短(52ms或者22ms甚至是Sync),那么优先级就变成了先执行当前任务,也就意味着上一个任务被打断了(interrupted)

被打断的任务会从当前节点开始往上推出context,因为在React只有一个stack,而下一个任务会从头开始的,所以在开始之前需要清空之前任务的的stack

context请看这里

unwindWork请看这里

然后重置所有的公共变量:

nextRoot = null;
nextRenderExpirationTime = NoWork;
nextLatestAbsoluteTimeoutMs = -1;
nextRenderDidError = false;
nextUnitOfWork = null;
# markPendingPriorityLevel

TODO: PriorityLevel有很多的时间变量,不知道具体的含义是什么

if (
  !isWorking ||
  isCommitting ||
  nextRoot !== root
)

这个判断条件就比较简单了,!isWorking || isCommitting简单来说就是要么处于没有work的状态,要么只能在render节点,不能处于commit阶段(比较好奇什么时候会在commit阶段有新的任务进来,commit都是同步的无法打断)。还有一个选项nextRoot !== root,这个的意思就是你的APP如果有两个不同的root,这时候也符合条件。

在符合条件之后就requestWork

function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
  const root = scheduleWorkToRoot(fiber, expirationTime);

  if (enableSchedulerTracing) {
    storeInteractionsForExpirationTime(root, expirationTime, true);
  }

  if (
    !isWorking &&
    nextRenderExpirationTime !== NoWork &&
    expirationTime < nextRenderExpirationTime
  ) {
    // This is an interruption. (Used for performance tracking.)
    interruptedBy = fiber;
    resetStack();
  }
  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);
  }
}

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;
  if (node === null && fiber.tag === HostRoot) {
    return fiber.stateNode;
  }
  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) {
      return node.stateNode;
    }
    node = node.return;
  }
  return null;
}

function resetStack() {
  if (nextUnitOfWork !== null) {
    let interruptedWork = nextUnitOfWork.return;
    while (interruptedWork !== null) {
      unwindInterruptedWork(interruptedWork);
      interruptedWork = interruptedWork.return;
    }
  }

  nextRoot = null;
  nextRenderExpirationTime = NoWork;
  nextLatestAbsoluteTimeoutMs = -1;
  nextRenderDidError = false;
  nextUnitOfWork = null;
}

# requestWork

addRootToSchedule把root加入到调度队列,但是要注意一点,不会存在两个相同的root前后出现在队列中

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

可以看出来,如果第一次调用addRootToSchedule的时候,nextScheduledRootnull,这时候公共变量firstScheduledRootlastScheduledRoot也是null,所以会把他们都赋值成root,同时root.nextScheduledRoot = root。然后第二次进来的时候,如果前后root是同一个,那么之前的firstScheduledRootlastScheduledRoot都是root,所以lastScheduledRoot.nextScheduledRoot = root就等于root.nextScheduledRoot = root

这么做是因为同一个root不需要存在两个,因为前一次调度如果中途被打断,下一次调度进入还是从同一个root开始,就会把新的任务一起执行了。

之后根据expirationTime调用performSyncWork还是scheduleCallbackWithExpirationTime

scheduleCallbackWithExpirationTime是根据时间片来执行任务的,会涉及到requestIdleCallback,详细解析看这里

isBatchingUpdatesisUnbatchingUpdates涉及到事件系统,看React事件系统

他们最终都要调用performWork

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

  // TODO: Get rid of Sync and use current time?
  if (expirationTime === Sync) {
    performSyncWork();
  } else {
    scheduleCallbackWithExpirationTime(root, expirationTime);
  }
}

# performWork

这里判断是否有deadline来分成两种渲染方式,但最大的差距其实是while循环的判断条件,有deadline的多了一个条件(!deadlineDidExpire || currentRendererTime >= nextFlushedExpirationTime)

我们先来看相同的部分

nextFlushedRoot !== null &&
nextFlushedExpirationTime !== NoWork &&
(minExpirationTime === NoWork ||
  minExpirationTime >= nextFlushedExpirationTime)

下一个输出节点不是null,并且过期时间不是NoWork,同时超时时间是NoWork,或者超时时间大于下个节点的超时时间

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;

    if (enableUserTimingAPI) {
      const didExpire = nextFlushedExpirationTime < currentRendererTime;
      const timeout = expirationTimeToMs(nextFlushedExpirationTime);
      stopRequestCallbackTimer(didExpire, timeout);
    }

    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();
}
ON THIS PAGE