深度解析React-render阶段:递与归

11/24/2023 React

对于 React 的 render 阶段可以分为两个部分,递和归。

这里我将从createRoot().render方法开始切入。这也是 React 18 的一个更新点。以前是通过 ReactDOM.render 去挂在 react 的框架,现在要先 createRoot,然后再执行 render 方法。最主要的是对于事件系统的更新,而不去污染 Document 元素。

# React 的优先级机制

React 渲染阶段包含三个优先级机制,分别是:

  1. React 的 Lane 优先级
  2. React 的事件优先级
  3. Scheduler 的优先级

React 的 Lane 使用二进制表示,共有 31 个值,通过位运算判断优先级,这提高了性能。Lane 优先级主要用于 React 实现一些 API 时的调度。

React 的事件优先级分为 5 个,是开发者在使用 React 时涉及最多的优先级机制。在触发事件回调时,根据事件类型分配不同的优先级。这些事件优先级和 Scheduler 的优先级可以互相转换。

Scheduler 的优先级是用于调度使用,具体可以看调度器文章

ReactFiberWorkLoop.js - requestUpdateLane方法

// Updates originating inside certain React methods, like flushSync, have
// their priority set by tracking it with a context variable.
//
// The opaque type returned by the host config is internally a lane, so we can
// use that directly.
// TODO: Move this type conversion to the event priority module.
const updateLane: Lane = (getCurrentUpdatePriority(): any);
if (updateLane !== NoLane) {
  return updateLane;
}

# 更新

当 React 涉及更新的时候,React 会创建一个 update 的对象,并 lane 优先级赋予给 fiber(当然 fiber 会取最高的优先级),最终通过 ensureRootIsScheduledscheduleTaskForRootDuringMicrotask 函数进入调度器进行任务调度。

调度器会通过优先级判断,进入 performConcurrentWorkOnRoot,根据情况选择是使用 Concurrent 模式还是 Sync 模式进行虚拟 DOM 的“递归”操作。

// We disable time-slicing in some cases: if the work has been CPU-bound
// for too long ("expired" work, to prevent starvation), or we're in
// sync-updates-by-default mode.
// TODO: We only check `didTimeout` defensively, to account for a Scheduler
// bug we're still investigating. Once the bug in Scheduler is fixed,
// we can remove this, since we track expiration ourselves.
const shouldTimeSlice =
  !includesBlockingLane(root, lanes) &&
  !includesExpiredLane(root, lanes) &&
  (disableSchedulerTimeoutInWorkLoop || !didTimeout);
let exitStatus = shouldTimeSlice
  ? renderRootConcurrent(root, lanes)
  : renderRootSync(root, lanes);

主要关注 includesBlockingLane 函数,用于判断是否进入 Concurrent 模式。对于用户的一些离散事件,会通过 Sync 模式快速响应。只有遇到 startTransitoin, useTranstiion, **useDeferredValue**这些函数的时候才会进入 Concurrent 模式,而 Concurrent 模式下,会以 5ms 进行时间分片,如果超过 5ms 的计算,就会去判断是否有其他更高优先级的任务。

export function includesBlockingLane(root: FiberRoot, lanes: Lanes): boolean {
  if (
    allowConcurrentByDefault &&
    (root.current.mode & ConcurrentUpdatesByDefaultMode) !== NoMode
  ) {
    // Concurrent updates by default always use time slicing.
    return false;
  }
  const SyncDefaultLanes =
    InputContinuousHydrationLane |
    InputContinuousLane |
    DefaultHydrationLane |
    DefaultLane; // React的事件优先级均属于这几个事件
  return (lanes & SyncDefaultLanes) !== NoLanes;
}

不管最后进入 Concurrent 模式还是 Sync 模式,都会进入 workLoop,这是 render 的“递”入口。

function workLoopSync() {
  // Perform work without checking if we need to yield between fiber.
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

function workLoopConcurrent() {
  // Perform work until Scheduler asks us to yield
  while (workInProgress !== null && !shouldYield()) {
    // $FlowFixMe[incompatible-call] found when upgrading Flow
    performUnitOfWork(workInProgress);
  }
}

#

workLoop 中,通过 performUnitOfWork 进行递归操作。在每次循环中,调用 beginWork,该函数首先判断当前 Fiber 的新旧 props 是否改变,以及 context 是否改变。如果没有改变,设置 didReceiveUpdate 为 false。然后根据当前 Fiber 的 tag 属性进入不同类型的方法中处理。

在每个类型方法的处理函数中,判断是更新还是挂载( current ≠== null)。如果 didReceiveUpdate 为 false,则执行 bailoutOnAlreadyFinishedWork,标记这个 Fiber 没有更新,并克隆它的 child 节点进行操作。

function bailoutOnAlreadyFinishedWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes
): Fiber | null {
  if (current !== null) {
    // Reuse previous dependencies
    workInProgress.dependencies = current.dependencies;
  }

  if (enableProfilerTimer) {
    // Don't update "base" render times for bailouts.
    stopProfilerTimerIfRunning(workInProgress);
  }

  markSkippedUpdateLanes(workInProgress.lanes);

  // Check if the children have any pending work.
  if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
    // The children don't have any work either. We can skip them.
    // TODO: Once we add back resuming, we should check if the children are
    // a work-in-progress set. If so, we need to transfer their effects.

    if (enableLazyContextPropagation && current !== null) {
      // Before bailing out, check if there are any context changes in
      // the children.
      lazilyPropagateParentContextChanges(current, workInProgress, renderLanes);
      if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
        return null;
      }
    } else {
      return null;
    }
  }

  // This fiber doesn't have work, but its subtree does. Clone the child
  // fibers and continue.
  cloneChildFibers(current, workInProgress);
  return workInProgress.child;
}

如果有存在更新,则会执行 reconcileChildren 函数的操作。

这个函数也是 React 的 render 阶段中最为重要的方法。先判断是挂载还是更新,两者的区别在于,挂载不会执行 delete(删除),place(替换)等操作。

接下来 React 的核心diff 算法来处理 React 的渲染,给每个涉及修改的 Fiber 都打上 flag,用于后续 commit 的时候一起执行真实 DOM 树的更新。

当整棵树遍历完成后,beginWork 将返回 null,表示 render 的递操作完成,进入 completeWork 函数,这是 render 阶段的操作。

#

操作,也就是 beginWork 中,是从 root 节点开始往底部遍历,而操作中,completeWork 则从底部开始往 Root 节点返回。

该函数较为简单,主要执行 bubbleProperties 函数,将优先级和子树打上的 flag 同步到父节点中,一直到 Root 节点。

这样有助于后续的 commit 进行树遍历时,知道哪颗子树需要更新,哪个不需要。

ps: 在以前的版本中,会形成一个链表,在 completeWork 中遍历链表执行 commit 操作。

function bubbleProperties(completedWork: Fiber) {
  const didBailout =
    completedWork.alternate !== null &&
    completedWork.alternate.child === completedWork.child;

  let newChildLanes = NoLanes;
  let subtreeFlags = NoFlags;

  if (!didBailout) {
    // Bubble up the earliest expiration time.
    if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) {
      // In profiling mode, resetChildExpirationTime is also used to reset
      // profiler durations.
      let actualDuration = completedWork.actualDuration;
      let treeBaseDuration = ((completedWork.selfBaseDuration: any): number);

      let child = completedWork.child;
      while (child !== null) {
        newChildLanes = mergeLanes(
          newChildLanes,
          mergeLanes(child.lanes, child.childLanes)
        );

        subtreeFlags |= child.subtreeFlags;
        subtreeFlags |= child.flags;

        // When a fiber is cloned, its actualDuration is reset to 0. This value will
        // only be updated if work is done on the fiber (i.e. it doesn't bailout).
        // When work is done, it should bubble to the parent's actualDuration. If
        // the fiber has not been cloned though, (meaning no work was done), then
        // this value will reflect the amount of time spent working on a previous
        // render. In that case it should not bubble. We determine whether it was
        // cloned by comparing the child pointer.
        // $FlowFixMe[unsafe-addition] addition with possible null/undefined value
        actualDuration += child.actualDuration;

        // $FlowFixMe[unsafe-addition] addition with possible null/undefined value
        treeBaseDuration += child.treeBaseDuration;
        child = child.sibling;
      }

      completedWork.actualDuration = actualDuration;
      completedWork.treeBaseDuration = treeBaseDuration;
    } else {
      let child = completedWork.child;
      while (child !== null) {
        newChildLanes = mergeLanes(
          newChildLanes,
          mergeLanes(child.lanes, child.childLanes)
        );

        subtreeFlags |= child.subtreeFlags;
        subtreeFlags |= child.flags;

        // Update the return pointer so the tree is consistent. This is a code
        // smell because it assumes the commit phase is never concurrent with
        // the render phase. Will address during refactor to alternate model.
        child.return = completedWork;

        child = child.sibling;
      }
    }

    completedWork.subtreeFlags |= subtreeFlags;
  } else {
    // Bubble up the earliest expiration time.
    if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) {
      // In profiling mode, resetChildExpirationTime is also used to reset
      // profiler durations.
      let treeBaseDuration = ((completedWork.selfBaseDuration: any): number);

      let child = completedWork.child;
      while (child !== null) {
        newChildLanes = mergeLanes(
          newChildLanes,
          mergeLanes(child.lanes, child.childLanes)
        );

        // "Static" flags share the lifetime of the fiber/hook they belong to,
        // so we should bubble those up even during a bailout. All the other
        // flags have a lifetime only of a single render + commit, so we should
        // ignore them.
        subtreeFlags |= child.subtreeFlags & StaticMask;
        subtreeFlags |= child.flags & StaticMask;

        // $FlowFixMe[unsafe-addition] addition with possible null/undefined value
        treeBaseDuration += child.treeBaseDuration;
        child = child.sibling;
      }

      completedWork.treeBaseDuration = treeBaseDuration;
    } else {
      let child = completedWork.child;
      while (child !== null) {
        newChildLanes = mergeLanes(
          newChildLanes,
          mergeLanes(child.lanes, child.childLanes)
        );

        // "Static" flags share the lifetime of the fiber/hook they belong to,
        // so we should bubble those up even during a bailout. All the other
        // flags have a lifetime only of a single render + commit, so we should
        // ignore them.
        subtreeFlags |= child.subtreeFlags & StaticMask;
        subtreeFlags |= child.flags & StaticMask;

        // Update the return pointer so the tree is consistent. This is a code
        // smell because it assumes the commit phase is never concurrent with
        // the render phase. Will address during refactor to alternate model.
        child.return = completedWork;

        child = child.sibling;
      }
    }

    completedWork.subtreeFlags |= subtreeFlags;
  }

  completedWork.childLanes = newChildLanes;

  return didBailout;
}

在执行完“递归”操作后,React 的 Render 阶段结束,接下来进入 commit 阶段,这个阶段是无法被打断的。