Skip to content

completeUnitOfWork

这个阶段的主要特点是:

  1. 产生 stateNode (当前节点对应的 DOM,此时只是保存在内存中)
  2. 标记 flags 和 subtreeFlags (在 Commit 阶段作为判断当前分支是否有 DOM 的操作)

栗子

仍然是 beginWork 的那个栗子

image

初次渲染

在初次渲染的时候,我们根据 beginWork 按照深度遍历优先的流程知道其当 performUnitOfWork 执行到 第一个没有 child 的节点(this is Comp21 的 Text 节点),因为 next 为 null 进行第一个 completeWork 的流程

代码如下

js
function completeUnitOfWork(unitOfWork: Fiber): void {
  // 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.
  let completedWork = 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.
    // 当前Node的原FiberNode实例对象
    const current = completedWork.alternate;
    // 父节点
    const returnFiber = completedWork.return;

    // Check if the work completed or if something threw.
    if ((completedWork.flags & Incomplete) === NoFlags) {
      let next;
      if (
        !enableProfilerTimer ||
        (completedWork.mode & ProfileMode) === NoMode
      ) {
        next = completeWork(current, completedWork, subtreeRenderLanes);
      } else {
        next = completeWork(current, completedWork, subtreeRenderLanes);
      }

      if (next !== null) {
        // Completing this fiber spawned new work. Work on that next.
        workInProgress = next;
        return;
      }
    }
    // 如果存在兄弟节点 那么将兄弟节点赋给workInProgress 进入到 兄弟节点的 workLoop 的流程
    const siblingFiber = completedWork.sibling;
    if (siblingFiber !== null) {
      // If there is more work to do in this returnFiber, do that next.
      workInProgress = siblingFiber;
      return;
    }
    // Otherwise, return to the parent
    // 如果 不存在兄弟节点 那么通过 do{} 循环执行 父节点的 completeWork流程
    //  此时 completedWork指向了父节点,那么在父节点完成 completeWork流程 时也会判断其是否存在兄弟节点,这样形成一个深度优先的链表 遍历流程
    completedWork = returnFiber;
    // Update the next thing we're working on in case something throws.
    workInProgress = completedWork;
  } while (completedWork !== null);

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

completeUnitOfWork(unitOfWork: Fiber)

不考虑 completeWork 的情况,可以看出来 completeUnitOfWork 的主要作用是 配合 beginWork 完成

  1. 有兄弟节点的情况下,将 workInProgress 指向兄弟节点
  2. 没有兄弟节点的情况下,执行父节点的 completeWork

image

‍ 同时根据 (completedWork.flags & Incomplete) === NoFlags 判断当前节点是否存在未完成的副作用,从而决定是执行completeWork还是unwindWork

completeWork(current, completedWork, subtreeRenderLanes)

节点不存在未完成副作用的情况下,对节点进行 DOM 等操作

  1. 产生 stateNode (当前节点对应的 DOM,此时只是保存在内存中)
  2. 标记 flags 和 subtreeFlags (在 Commit 阶段作为判断当前分支是否有 DOM 的操作)

image

bubbleProperties()

在 completeWork 中 对于大部分节点都执行了一个 bubbleProperties()方法 , 其作用是通过subtreeFlags替代之前的 finishWork.firstEffect 副作用链表,来标记当前子树中存在副作用,从而避免在 commit 的时候再次深度遍历寻找

  1. 在 FiberNode 树上通过 FiberNode.subtreeFlags , FiberNode.flags 标记节点和子孙节点 DOM 操作 类型
  • FiberNode.flags 标记当前节点 DOM 的操作 更新、创建、删除等
  • FiberNode.subtreeFlags 通过位运算标记当前节点树下子孙包含的操作类型

注意: 其跟 FiberNode.lanes 和 FiberNode.childLanes 很像

  1. 更新 FiberNode.childLanes
  • 合并当前节点的 lanes 和 childLanes (completedWork.childLanes = mergeLanes(newChildLanes, mergeLanes(child.lanes, child.childLanes)))

在 performUnitOfWork 的时候,Update 的 FiberNode 标记的是 lanes ,祖先节点 标记点 childLanes。 那么这时候通过 lanes 和 childLanes 的合并,从而更好的根据优先级找到更新的节点树分支

js
/**
 * 1. 在FiberNode树上通过FiberNode.subtreeFlags , FiberNode.flags 标记节点和子孙节点 DOM操作 类型
 *   - FiberNode.flags 标记当前节点DOM的操作  更新、创建、删除等
 *   - FiberNode.subtreeFlags 通过位运算标记当前节点树下子孙包含的操作类型
 *  注意: 其跟 FiberNode.lanes 和 FiberNode.childLanes 很像
 *  2. 更新 FiberNode.childLanes
 *    合并当前节点的 lanes 和 childLanes (completedWork.childLanes = mergeLanes(newChildLanes, mergeLanes(child.lanes, child.childLanes)))
 *    在performUnitOfWork的时候,Update的FiberNode标记的是lanes ,祖先节点 标记点childLanes。
 *    那么这时候通过lanes 和 childLanes的合并,从而更好的根据优先级找到更新的节点树分支
 * @param {*} completedWork
 * @returns
 */
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) {
    } else {
      let child = completedWork.child;
      while (child !== null) {
        // 合并当前节点的 lanes 和 childLanes
        newChildLanes = mergeLanes(
          newChildLanes,
          mergeLanes(child.lanes, child.childLanes)
        );
        // 跟 FiberNode.lanes 和 FiberNode.childLanes 很像
        // 通过位运算标记当前节点树下子孙包含的操作类型
        subtreeFlags |= child.subtreeFlags;
        subtreeFlags |= child.flags;

        child = child.sibling;
      }
    }
    // 通过位运算标记当前节点树下子孙包含的操作类型
    completedWork.subtreeFlags |= subtreeFlags;
  }
  completedWork.childLanes = newChildLanes;
  return didBailout;
}

HostComponent

js
 case HostComponent: {
      popHostContext(workInProgress);
      // 获取 Root 的 Container (div#root)
      const rootContainerInstance = getRootHostContainer();
      const type = workInProgress.type;
      // current 和 stateNode 都不为空  说明是更新流程
      if (current !== null && workInProgress.stateNode != null) {
        // 更新 DOM
        updateHostComponent(
          current,
          workInProgress,
          type,
          newProps,
          rootContainerInstance,
        );
        // 标记是否存在 ref 属性的 更新
        if (current.ref !== workInProgress.ref) {
          markRef(workInProgress);
        }
      } else {
        const currentHostContext = getHostContext();
        // TODO: Move createInstance to beginWork and keep it on a context
        // "stack" as the parent. Then append children as we go in beginWork
        // or completeWork depending on whether we want to add them top->down or
        // bottom->up. Top->down is faster in IE11.
        // 服务端渲染有关
        const wasHydrated = popHydrationState(workInProgress);
        if (wasHydrated) {
          // 服务端渲染
        } else {
          // 生成 DOM 元素
          const instance = createInstance(
            type,
            newProps,
            rootContainerInstance,
            currentHostContext,
            workInProgress,
          );
          // 将子元素的DOM添加到当前DOM元素下
          appendAllChildren(instance, workInProgress, false, false);

          // 缓存DOM元素
          workInProgress.stateNode = instance;

          // Certain renderers require commit-time effects for initial mount.
          // (eg DOM renderer supports auto-focus for certain elements).
          // Make sure such renderers get scheduled for later work.
          // 处理DOM元素的属性,如果遇到autoFocus 并标记副作用
          if (
            finalizeInitialChildren(
              instance,
              type,
              newProps,
              rootContainerInstance,
              currentHostContext,
            )
          ) {
            // 标记 flags  Update
            markUpdate(workInProgress);
          }
        }

        if (workInProgress.ref !== null) {
          // If there is a ref on a host node we need to schedule a callback
          // 标记 flags  Ref
          markRef(workInProgress);
        }
      }
      return null;
    }

分为更新流程 和 创建流程(SSR)

  1. 更新流程(current !== null && workInProgress.stateNode != null )

    • updateHostComponent

      其核心是一个 prepareUpdate(instance,type ,oldProps, newProps, rootContainerInstance , currentHostContext ),并将需要更新的信息存在 workInProgress.updateQueue中

      markUpdate(workInProgress); 标记有更新

    • ref 更新的标识 markRef(workInProgress);

HostText

文本类型的 FiberNode 的 completeWork

js
case HostText: {
      const newText = newProps;
      if (current && workInProgress.stateNode != null) {
        const oldText = current.memoizedProps;
        // If we have an alternate, that means this is an update and we need
        // to schedule a side-effect to do the updates.
        updateHostText(current, workInProgress, oldText, newText);
      } else {
        if (typeof newText !== 'string') {
          invariant(
            workInProgress.stateNode !== null,
            'We must have new props for new mounts. This error is likely ' +
              'caused by a bug in React. Please file an issue.',
          );
          // This can happen when we abort work.
        }
        const rootContainerInstance = getRootHostContainer();
        const currentHostContext = getHostContext();
        const wasHydrated = popHydrationState(workInProgress);
        if (wasHydrated) {
          if (prepareToHydrateHostTextInstance(workInProgress)) {
            markUpdate(workInProgress);
          }
        } else {
          workInProgress.stateNode = createTextInstance(
            newText,
            rootContainerInstance,
            currentHostContext,
            workInProgress,
          );
        }
      }
      bubbleProperties(workInProgress);
      return null;
    }

副作用标记和副作用链

React17

在旧的任务调度中,如果在 completeWork 阶段发生 DOM 的操作(更新、添加、删除等),都会在 FiberNode.flags 上标记。

同时触发

js
      /**
       * 在根节点上按照深度优先的规则形成一个
       *    firstEffect指向第一个有副作用的DOM
       *    lastEffect指向最后一个有副作用的DOM
       *    中间更新的DOM 通过 firstEffect.nextEffect.xxxxxx 形成一个单链
       * 因为 CompleteWork是一个由下而上的过程,所以通过
       *  将当前节点completedWork.firstEffect保存到父节点的 lastEffect.nextEffect(单链)。
       *  然后一层层往上传播,直到 returnFiber !== null 即遇到根节点截止。
       *  这样就可以在根节点上收集到所有的产生副作用的节点 FiberNode
       *
       */
      if (
        returnFiber !== null &&
        // Do not append effects to parents if a sibling failed to complete
        (returnFiber.flags & Incomplete) === NoFlags
      ) {
        // 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.
        // 如果父节点的 firstEffect 为null 所以暂时没有,所以当前作为第一个
        if (returnFiber.firstEffect === null) {
          returnFiber.firstEffect = completedWork.firstEffect;
        }
        // 如果当前节点的lastEffect有值
        //   1. 如果父节点的 lastEffect 有值,那么就将当前Node变成 lastEffect.nextEffect 父节点的 lastEffect变成本身
        //   2. 如果没有值,那么直接将父节点的lastEffect 变成本身
        if (completedWork.lastEffect !== null) {
          if (returnFiber.lastEffect !== null) {
            returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
          }
          returnFiber.lastEffect = completedWork.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 flags = completedWork.flags;

        // Skip both NoWork and PerformedWork tags when creating the effect
        // list. PerformedWork effect is read by React DevTools but shouldn't be
        // committed.
        // 如果当前节点产生DOM的操作
        if (flags > PerformedWork) {
          // 将当前节点加入到父节点.firstEffect
          // 1. 如果当前节点同一层级的前面节点没有副作用,那么这时候 父节点.firstEffect == 父节点.lastEffect == null
          //      将当前节点加入到 父节点.firstEffect == 父节点.lastEffect == completedWork
          // 2. 如果当前节点同一层级的前面节点也有副作用,那么其不修改父节点..lastEffect.nextEffect = completedWork ; 父节点.lastEffect = completedWork;
          //      这样父节点的 fistEffect.nextEffect.nextEffect  === lastEffect
          //
          if (returnFiber.lastEffect !== null) {
            returnFiber.lastEffect.nextEffect = completedWork;
          } else {
            returnFiber.firstEffect = completedWork;
          }
          returnFiber.lastEffect = completedWork;
        }

image

这样在触发点击事件的时候在跟 FiberNode.alternate 上就可以看到一个

image

React18

在新版的任务调度中 对于副作用的标记主要通过 flags subtreeFlags 去标记副作用节点分支,舍弃了之前的根节点通过副作用firstEffect、lastEffect、nextEffect 形成的单链结构,去找到当前需要更新的 DOM 元素的 FiberNode 节点。

而是使用 flags subtreeFlags 位运算的操作去深度遍历标记