React 源码中最重要的部分

2024-01-19 16:15:48 浏览数 (1)

React 知命境第 43 篇,原创第 156 篇

无论是并发模式,还是同步模式,最终要生成新的 Fiber Tree,都是通过遍历 workInProgress 的方式去执行 performUnitOfWork

代码语言:javascript复制
// 并发模式
function workLoopConcurrent() {
  // Perform work until Scheduler asks us to yield
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}
代码语言:javascript复制
// 同步
function workLoopSync() {
  // Already timed out, so perform work without checking if we need to yield.
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

需要特别注意的是这里的 workInProgress 表示当前正在执行的 Fiber 节点,他会在递归的过程中不断改变指向,这里要结合我们之前章节中分享过的 Fiber Tree 的链表结构来理解

代码语言:javascript复制
if (next === null) {
  // If this doesn't spawn new work, complete the current work.
  completeUnitOfWork(unitOfWork);
} else {
  workInProgress = next;
}

performUnitOfWork

该方法主要用于创建 Fiber Tree,是否理解 Fiber Tree 的构建过程,跟我们是否能做好性能优化有非常直接的关系,因此对我而言,这是 React 源码中最重要的一个部分。

从他的第一行代码我们就能知道,Fiber Tree 的创建是依赖于双缓存策略。上一轮构建完成的 Fiber tree,在代码中用 current 来表示。

正在构建中的 Fiber tree,在代码中用 workInProgress 来表示,并且他们之间同层节点都用 alternate 相互指向。

代码语言:javascript复制
current.alternate = workInProgress;
workInProgress.alternate = current;

workInProgress 会基于 current 构建。

代码语言:javascript复制
function performUnitOfWork(unitOfWork: Fiber): void {
  const current = unitOfWork.alternate;
  ...

整体的思路是从 current[rootFiber] 树往下执行深度遍历,在遍历的过程中,会根据 key、props、context、state 等条件进行判断,判断结果如果发现节点没有发生变化,那么就复用 current 的节点,如果发生了变化,则重新创建 Fiber 节点,并标记需要修改的类型,用于传递给 commitRoot

beginWork

每一个被遍历到的 Fiber 节点,会执行 beginWork 方法

代码语言:javascript复制
let next;
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
  startProfilerTimer(unitOfWork);
  next = beginWork(current, unitOfWork, subtreeRenderLanes);
  stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
} else {
  next = beginWork(current, unitOfWork, subtreeRenderLanes);
}

该方法根据传入的 Fiber 节点创建子节点,并将这两个节点连接起来

代码语言:javascript复制
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {...}

React 在 ReactFiberBeginWork.new.js 模块中维护了一个全局的 didReceiveUpdate 变量,来表示当前节点是否需要更新

代码语言:javascript复制
let didReceiveUpdate: boolean = false;

在 beginWork 的执行过程中,会经历一些判断来确认 didReceiveUpdate 的值,从而判断该 Fiber 节点是否需要重新执行

代码语言:javascript复制
  if (current !== null) {
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;

    if (
      oldProps !== newProps ||
      hasLegacyContextChanged() ||
      // Force a re-render if the implementation changed due to hot reload:
      (__DEV__ ? workInProgress.type !== current.type : false)
    ) {
      // If props or context changed, mark the fiber as having performed work.
      // This may be unset if the props are determined to be equal later (memo).
      didReceiveUpdate = true;
    } else {

这里比较的是 props 和 context 是否发生了变化。当他们其中一个变化时,则将 didReceiveUpdate 设置为 true

这里的 hasLegacyContextChanged() 兼容的是旧版本 的 context,新版本的 context 是否发生变化会反应到 pending update 中,也就是使用下面的 checkScheduledUpdateOrContext 来查看是否有更新的调度任务

当 props 和 context 都没有发生变化,并且也不存在对应的调度任务时,将其设置为 false

如果有 state/context 发生变化,则会存在调度任务

代码语言:javascript复制
} else {
  // Neither props nor legacy context changes. Check if there's a pending
  // update or context change.
  const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
    current,
    renderLanes,
  );
  if (
    !hasScheduledUpdateOrContext &&
    // If this is the second pass of an error or suspense boundary, there
    // may not be work scheduled on `current`, so we check for this flag.
    (workInProgress.flags & DidCapture) === NoFlags
  ) {
    // No pending updates or context. Bail out now.
    didReceiveUpdate = false;
    return attemptEarlyBailoutIfNoScheduledUpdate(
      current,
      workInProgress,
      renderLanes,
    );
  }

这里有一个很关键的点,就在于当方法进入到 attemptEarlyBailoutIfNoScheduledUpdate 去判断子节点是否可以 bailout 时,他并没有比较子节点的 props

核心的逻辑在 bailoutOnAlreadyFinishedWork 中。

代码语言:javascript复制
{
  ...
  // 判断子节点是否有 pending 任务要做
  if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
    // The children don't have any work either. We can skip them.
    return null
  }

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

所以这里有一个很重要的思考就是为什么判断子节点是否发生变化时,并没有去比较 props,这是性能优化策略的关键一步,结合我们之前讲的性能优化策略去理解,你就能知道答案

回到 beginWork, 后续的逻辑会根据不同的 tag,创建不同类型的 Fiber 节点

代码语言:javascript复制
switch (workInProgress.tag) {
  case IndeterminateComponent: {
    return mountIndeterminateComponent(
      current,
      workInProgress,
      workInProgress.type,
      renderLanes,
    );
  }
  case LazyComponent: {
    const elementType = workInProgress.elementType;
    return mountLazyComponent(
      current,
      workInProgress,
      elementType,
      renderLanes,
    );
  }
  case FunctionComponent: {
    const Component = workInProgress.type;
    const unresolvedProps = workInProgress.pendingProps;
    const resolvedProps =
      workInProgress.elementType === Component
        ? unresolvedProps
        : resolveDefaultProps(Component, unresolvedProps);
    return updateFunctionComponent(
      current,
      workInProgress,
      Component,
      resolvedProps,
      renderLanes,
    );
  }
  case ClassComponent: {
    const Component = workInProgress.type;
    const unresolvedProps = workInProgress.pendingProps;
    const resolvedProps =
      workInProgress.elementType === Component
        ? unresolvedProps
        : resolveDefaultProps(Component, unresolvedProps);
    return updateClassComponent(
      current,
      workInProgress,
      Component,
      resolvedProps,
      renderLanes,
    );
  }
  case HostRoot:
    return updateHostRoot(current, workInProgress, renderLanes);
  case HostComponent:
    return updateHostComponent(current, workInProgress, renderLanes);
  case HostText:
    return updateHostText(current, workInProgress);
  case SuspenseComponent:
    return updateSuspenseComponent(current, workInProgress, renderLanes);
  case HostPortal:
    return updatePortalComponent(current, workInProgress, renderLanes);
  case ForwardRef: {
    const type = workInProgress.type;
    const unresolvedProps = workInProgress.pendingProps;
    const resolvedProps =
      workInProgress.elementType === type
        ? unresolvedProps
        : resolveDefaultProps(type, unresolvedProps);
    return updateForwardRef(
      current,
      workInProgress,
      type,
      resolvedProps,
      renderLanes,
    );
  }
  
  // ...
  
  case MemoComponent: {
    const type = workInProgress.type;
    const unresolvedProps = workInProgress.pendingProps;
    // Resolve outer props first, then resolve inner props.
    let resolvedProps = resolveDefaultProps(type, unresolvedProps);
    if (__DEV__) {
      if (workInProgress.type !== workInProgress.elementType) {
        const outerPropTypes = type.propTypes;
        if (outerPropTypes) {
          checkPropTypes(
            outerPropTypes,
            resolvedProps, // Resolved for outer only
            'prop',
            getComponentNameFromType(type),
          );
        }
      }
    }
    resolvedProps = resolveDefaultProps(type.type, resolvedProps);
    return updateMemoComponent(
      current,
      workInProgress,
      type,
      resolvedProps,
      renderLanes,
    );
  }
}
// ... 其他类型

我们重点关注 updateFunctionComponent 的执行逻辑,可以发现,当 didReceiveUpdate 为 false 时,会执行 bailout 跳过创建过程

代码语言:javascript复制
if (current !== null && !didReceiveUpdate) {
  bailoutHooks(current, workInProgress, renderLanes);
  return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}

如果无法 bailout,则最后执行 reconcileChildren 创建新的子节点

代码语言:javascript复制
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  return workInProgress.child;

另外我们还应该关注 updateMemoComponent 中的逻辑。该逻辑通过浅比较函数 shallowEqual 来比较更新前后两个 props 的差异。当比较结果为 true 时,也是调用 bailout 跳过创建。

而不是沿用 didReceiveUpdate 的结果

代码语言:javascript复制
if (!hasScheduledUpdateOrContext) {
  // This will be the props with resolved defaultProps,
  // unlike current.memoizedProps which will be the unresolved ones.
  const prevProps = currentChild.memoizedProps;
  // Default to shallow comparison
  let compare = Component.compare;
  compare = compare !== null ? compare : shallowEqual;
  if (compare(prevProps, nextProps) && current.ref === workInProgress.ref) {
    return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
  }
}

completeWork

performUnitOfWork 执行过程中,当发现当前节点已经没有子节点了,就会调用 completeUnitOfWork 方法

代码语言:javascript复制
if (next === null) {
  // If this doesn't spawn new work, complete the current work.
  completeUnitOfWork(unitOfWork);

该方法主要用于执行 completeWork。completeWork 主要的作用是用于创建与 Fiber 节点对应的 DOM 节点。

这里创建的 DOM 节点并没有插入到 HTML 中,还存在于内存里

代码语言:javascript复制
const instance = createInstance(
  type,
  newProps,
  rootContainerInstance,
  currentHostContext,
  workInProgress,
);

appendAllChildren(instance, workInProgress, false, false);

由于 completeWork 的执行是从叶子节点,往根节点执行,因此,每次我们将新创建的节点 append 到父节点,执行到最后 rootFiber 时,一个完整的 DOM 树就已经构建完成了

completeWork 的执行顺序是一个回溯的过程

当然,Fiber 节点与 DOM 节点之间,也会保持一一对应的引用关系,因此在更新阶段,我们能够轻易的判断和复用已经存在的 DOM 节点从而避免重复创建

遍历顺序

beginWorkcompleteWork 的执行顺序理解起来比较困难,为了便于理解,我们这里用一个图示来表达

例如有这样一个结构的节点

代码语言:javascript复制
<div id="root">
  <div className="1">
    <div className="1-1">1-1</div>
    <div className="1-2">1-2</div>
    <div className="1-3">
      <div className="1-3-1">1-3-1</div>
    </div>
  </div>
  <div className="2">2</div>
  <div className="3">3</div>
</div>

beginWork 的执行是按照 Fiber 节点的链表深度遍历执行。

completeWork 则是当 fiber.next === null 时开始执行,他一个从叶子节点往根节点执行的回溯过程。当叶子节点被执行过后,则对叶子节点的父节点执行 completeWork

下图就是上面 demo 的执行顺序

其中蓝色圆代表对应节点的 beginWork 执行。黄色圆代表对应节点的 completeWork 执行。

总结

beginWorkcompleteWork 的执行是 React 源码中最重要的部分,理解他们的核心逻辑能有效帮助我们做好项目的性能优化。因此在学习他们的过程中,应该结合实践去思考优化策略。

不过性能优化的方式在我们之前的章节中已经详细介绍过,因此这里带大家阅读源码更多的是做一个验证,去揭开源码的神秘面纱。

到这篇文章这里,React 原理的大多数重要逻辑我们在知命境的文章都已经给大家分享过了,其中包括同步更新逻辑,异步更新逻辑,任务优先级队列,任务调度,Fiber 中的各种链表结构,各种比较方式的成本,包括本文介绍的 Fiber tree 的构建过程,大家可以把这些零散的文章串起来总结一下,有能力的可以自己在阅读源码时结合我分享的内容进一步扩展和完善。

阅读源码是一个高投入,低回报的过程,希望我的这些文章能有效帮助大家以更低的时间成本获得更高的知识回报。

0 人点赞