React源码学习进阶(六)completeWork究竟做了什么

本文采用React v16.13.1版本源码进行分析

源码解析

之前提到,在beginWork走到叶子节点,没有下一个子节点的时候,就会走到completeWork的流程:

function performUnitOfWork(unitOfWork: Fiber): Fiber | null {
  // 这里没有子节点了,走completeUnitOfWork
  if (next === null) {
    // If this doesn't spawn new work, complete the current work.
    next = completeUnitOfWork(unitOfWork);
  }

  ReactCurrentOwner.current = null;
  return next;
}

其中completeUnitOfWork逻辑如下:

function completeUnitOfWork(unitOfWork: Fiber): Fiber | null {
  // Attempt to complete the current unit of work, then move to the next
  // sibling. If there are no more siblings, return to the parent fiber.
  workInProgress = unitOfWork;
  do {
    // The current, flushed, state of this fiber is the alternate. Ideally
    // nothing should rely on this, but relying on it here means that we don't
    // need an additional field on the work in progress.
    const current = workInProgress.alternate;
    const returnFiber = workInProgress.return;

    // Check if the work completed or if something threw.
    if ((workInProgress.effectTag & Incomplete) === NoEffect) {
      setCurrentDebugFiberInDEV(workInProgress);
      let next;
      if (
        !enableProfilerTimer ||
        (workInProgress.mode & ProfileMode) === NoMode
      ) {
        next = completeWork(current, workInProgress, renderExpirationTime);
      } else {
        startProfilerTimer(workInProgress);
        next = completeWork(current, workInProgress, renderExpirationTime);
        // Update render duration assuming we didn't error.
        stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false);
      }
      stopWorkTimer(workInProgress);
      resetCurrentDebugFiberInDEV();
      resetChildExpirationTime(workInProgress);

      if (next !== null) {
        // Completing this fiber spawned new work. Work on that next.
        return next;
      }

      if (
        returnFiber !== null &&
        // Do not append effects to parents if a sibling failed to complete
        (returnFiber.effectTag & Incomplete) === NoEffect
      ) {
        // Append all the effects of the subtree and this fiber onto the effect
        // list of the parent. The completion order of the children affects the
        // side-effect order.
        if (returnFiber.firstEffect === null) {
          returnFiber.firstEffect = workInProgress.firstEffect;
        }
        if (workInProgress.lastEffect !== null) {
          if (returnFiber.lastEffect !== null) {
            returnFiber.lastEffect.nextEffect = workInProgress.firstEffect;
          }
          returnFiber.lastEffect = workInProgress.lastEffect;
        }

        // If this fiber had side-effects, we append it AFTER the children's
        // side-effects. We can perform certain side-effects earlier if needed,
        // by doing multiple passes over the effect list. We don't want to
        // schedule our own side-effect on our own list because if end up
        // reusing children we'll schedule this effect onto itself since we're
        // at the end.
        const effectTag = workInProgress.effectTag;

        // Skip both NoWork and PerformedWork tags when creating the effect
        // list. PerformedWork effect is read by React DevTools but shouldn't be
        // committed.
        if (effectTag > PerformedWork) {
          if (returnFiber.lastEffect !== null) {
            returnFiber.lastEffect.nextEffect = workInProgress;
          } else {
            returnFiber.firstEffect = workInProgress;
          }
          returnFiber.lastEffect = workInProgress;
        }
      }
    } else {
      // This fiber did not complete because something threw. Pop values off
      // the stack without entering the complete phase. If this is a boundary,
      // capture values if possible.
      const next = unwindWork(workInProgress, renderExpirationTime);

      // Because this fiber did not complete, don't reset its expiration time.

      if (
        enableProfilerTimer &&
        (workInProgress.mode & ProfileMode) !== NoMode
      ) {
        // Record the render duration for the fiber that errored.
        stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false);

        // Include the time spent working on failed children before continuing.
        let actualDuration = workInProgress.actualDuration;
        let child = workInProgress.child;
        while (child !== null) {
          actualDuration += child.actualDuration;
          child = child.sibling;
        }
        workInProgress.actualDuration = actualDuration;
      }

      if (next !== null) {
        // If completing this work spawned new work, do that next. We'll come
        // back here again.
        // Since we're restarting, remove anything that is not a host effect
        // from the effect tag.
        // TODO: The name stopFailedWorkTimer is misleading because Suspense
        // also captures and restarts.
        stopFailedWorkTimer(workInProgress);
        next.effectTag &= HostEffectMask;
        return next;
      }
      stopWorkTimer(workInProgress);

      if (returnFiber !== null) {
        // Mark the parent fiber as incomplete and clear its effect list.
        returnFiber.firstEffect = returnFiber.lastEffect = null;
        returnFiber.effectTag |= Incomplete;
      }
    }

    const siblingFiber = workInProgress.sibling;
    if (siblingFiber !== null) {
      // If there is more work to do in this returnFiber, do that next.
      return siblingFiber;
    }
    // Otherwise, return to the parent
    workInProgress = returnFiber;
  } while (workInProgress !== null);

  // We've reached the root.
  if (workInProgressRootExitStatus === RootIncomplete) {
    workInProgressRootExitStatus = RootCompleted;
  }
  return null;
}

这块逻辑初看有一些复杂,其实整个逻辑就是一个do...while

简化一下中间的逻辑,这里就是两个点:

  • 对当前的workInProgress执行completeWork逻辑

  • 遍历兄弟节点,如果有兄弟节点,则直接返回(意味着开启新一轮的performUnitWork),否则对父节点继续进行completeWork的流程。

接下来看一看completeWork的逻辑:

实际上可以看到,绝大多数组件的completeWork都是没有做逻辑的,我们核心关注下HostComponent的实现:

  • 调用createInstance,创建了一个DOM元素

  • 调用appendAllChildren,将所有的子节点加入进来

  • 将元素存入Fiber节点的stateNode当中

首先来看一下createInstance,它位于packages/react-dom/src/client/ReactDOMHostConfig.js

其实可以理解为就是调用document.createElement去创建了对应的dom元素。

接着看一下appendAllChildren的实现

这里的appendInitialChild其实可以理解为appendChild的实现,这段代码也就是将它的所有子节点,如果是DOM元素的全部append进来。

最后我们可以想象得到,在completeWork的阶段,Fiber树的索引顺序是从叶子节点往上遍历,因此整个DOM树其实是从completeWork开始逐步建立起来,最后索引到根节点为止。

小结一下

其实completeWork并没有做什么事情,重点是将DOM树在遍历过程中组建起来(但并未真正挂载),在后续的commit过程中进行真正的渲染挂载,它的整个遍历顺序可以通过下图来总结:

image-20220921094350335

可以看到,beginWork的是一个从上至下,从左至右的深度遍历顺序,而completeWork更像是一个从叶子节点由左至右,由下至上的遍历过程,比较类似于后续遍历的方式。

beginWork的时候我们从上至下进行的是reconciler流程,在completeWork的时候,我们是从下至上构建出DOM树。

最后用户能感知到的更新是统一进入commit阶段处理,在这个阶段一定是同步执行的,不可以被打断。这块内容下篇文章会详细分析。

Last updated