React 原理分析(三)—— Fiber 的commit 阶段

1,798 阅读5分钟

Commit 阶段概述

React 源码分析(二)—— Fiber 的 render 阶段 讲到, Fiberrender 阶段完成后,对应 FiberDOM 结构和 EffectList 也被顺利的挂载在Fiber上。当render阶段完成后,便进入了 Fibercommit 阶段。

function performConcurrentWorkOnRoot(root, didTimeout) {
    // render 阶段完成, 准备进入 commit 阶段
    root.finishedWork = finishedWork;
    root.finishedLanes = lanes;
    finishConcurrentRender(root, exitStatus, lanes);
}

function finishConcurrentRender(root, exitStatus, lanes) {
  switch (exitStatus) {
    case RootCompleted: {
      // The work completed. Ready to commit.
      commitRoot(root);
      break;
    }
  }
}

commitRootcommit 阶段的起点,commit 阶段为同步逻辑,不会使用concurrent模式的时间分片功能。commitRoot 的主要流程如下

commit 阶段详解

处理上次 render 产生的 Effect

function commitRootImpl(root, renderPriorityLevel) {
   do {
      flushPassiveEffects();
   } while (rootWithPendingPassiveEffects !== null);
 }

commit 阶段的开始时间会去处理上次 render 阶段注册 & 还未被完成的 effect 异步处理函数。while 循环保证如果异步函数中执行了新的 setState 能在 commit 前置完成。

挂载 Effect 的异步回调函数

如果 Root FibersubtreeFlagsflags 不为 NoFlags 的话,说明当前Root Fiber存在 Effect 需要执行。执行 flushPassiveEffects 注册异步回调函数。

subtreeFlags 的处理逻辑在 render 阶段中的 completeWork 阶段。

  if (
    (finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
    (finishedWork.flags & PassiveMask) !== NoFlags
  ) {
    if (!rootDoesHavePassiveEffects) {
      rootDoesHavePassiveEffects = true;
      pendingPassiveEffectsRemainingLanes = remainingLanes;
      scheduleCallback(NormalSchedulerPriority, () => {
        flushPassiveEffects();
        return null;
      });
    }
  }

重点是 flushPassiveEffects 函数,在处理上次 render 产生的 Effect 时已经调用过。他的作用是处理useEffectcreate/destory 逻辑。

export function flushPassiveEffects(): boolean {
      return flushPassiveEffectsImpl();
}

function flushPassiveEffectsImpl() {
  // 处理 hook 的 destory 函数
  commitPassiveUnmountEffects(root.current);
  // 处理 hook 的 create 函数
  commitPassiveMountEffects(root, root.current);
}

commitPassiveUnmountEffectscommitPassiveMountEffect的逻辑比较一致, 深度优先遍历,并在 begincomplete 执行不同的逻辑,这里以 commitPassiveUnmountEffects 为例子。

function commitPassiveUnmountEffects_begin() {
  while (nextEffect !== null) {
    const fiber = nextEffect;
    const child = fiber.child;
    if ((fiber.subtreeFlags & PassiveMask) !== NoFlags && child !== null) {
      ensureCorrectReturnPointer(child, fiber);
      nextEffect = child;
    } else {
      commitPassiveUnmountEffects_complete();
    }
  }
}

function commitPassiveUnmountEffects_complete() {
  while (nextEffect !== null) {
    const fiber = nextEffect;
    if ((fiber.flags & Passive) !== NoFlags) {
      commitPassiveUnmountOnFiber(fiber);
    }
    const sibling = fiber.sibling;
    if (sibling !== null) {
      nextEffect = sibling;
      return;
    }
    nextEffect = fiber.return;
  }
}

function commitPassiveUnmountOnFiber(finishedWork: Fiber): void {
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent: {
        // 执行 `EffectFlag` 为 HookPassive 的函数
        commitHookEffectListUnmount(
          HookPassive | HookHasEffect,
          finishedWork,
          finishedWork.return,
        );
    }
  }
}

BeforeMutation 阶段的处理

BeforeMutation 是 commit 流程的第一个阶段,这个阶段执行于 浏览区渲染DOM(Mutation阶段)之前。在这个阶段中会进行 getSnapshotBeforeUpdate 生命周期的处理。

beforeMutation的起点函数为commitBeforeMutatuonEffects

const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
  root,
  finishedWork,
);

commitBeforeMutatuonEffects 会从Root Fiber 用深度遍历交替执行 begincomplete 函数。

begin 阶段: 没有特殊的处理逻辑。

complete 阶段:去执行 commitBeforeMutationEffectsOnFiber 逻辑。这里的深度遍历逻辑与 render 阶段的 beginWork/completeWork 逻辑十分类似。

export function commitBeforeMutationEffects(
  root: FiberRoot,
  firstChild: Fiber,
) {
  ... 
  nextEffect = firstChild;
  commitBeforeMutationEffects_begin();
  ...
}

function commitBeforeMutationEffects_begin() {
  while (nextEffect !== null) {
    const fiber = nextEffect;
    const child = fiber.child;
    if (
      (fiber.subtreeFlags & BeforeMutationMask) !== NoFlags &&
      child !== null
    ) {
      ensureCorrectReturnPointer(child, fiber);
      nextEffect = child;
    } else {
      commitBeforeMutationEffects_complete();
    }
  }
}

function commitBeforeMutationEffects_complete() {
  while (nextEffect !== null) {
    const fiber = nextEffect;
    commitBeforeMutationEffectsOnFiber(fiber);
    const sibling = fiber.sibling;
    if (sibling !== null) {
      nextEffect = sibling;
      return;
    }
    nextEffect = fiber.return;
  }
}

commitBeforeMutationEffectsOnFiber 的逻辑比较简单,判断当前 Fiber 是否为存在 Snapshot 这个 Flag。如果有,则执行对应的getSnapshotBeforeUpdate 函数。

function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
  const current = finishedWork.alternate;
  const flags = finishedWork.flags;
  if ((flags & Snapshot) !== NoFlags) {
    switch (finishedWork.tag) {
      case FunctionComponent:
      case ForwardRef:
      case SimpleMemoComponent: 
      case HostComponent:
      case HostText:
      case HostPortal:
      case IncompleteClassComponent:{
      // Nothing to do for these component types
        break;
      }
      case ClassComponent: {
        if (current !== null) {
          const prevProps = current.memoizedProps;
          const prevState = current.memoizedState;
          const instance = finishedWork.stateNode;
          // We could update instance props and state here,
          // but instead we rely on them being set during last render.
          // TODO: revisit this when we implement resuming.
          const snapshot = instance.getSnapshotBeforeUpdate(
            finishedWork.elementType === finishedWork.type
              ? prevProps
              : resolveDefaultProps(finishedWork.type, prevProps),
            prevState,
          );
          instance.__reactInternalSnapshotBeforeUpdate = snapshot;
        }
        break;
      }
      case HostRoot: {
        if (supportsMutation) {
          const root = finishedWork.stateNode;
          clearContainer(root.containerInfo);
        }
        break;
      }
      default: {
        throw new Error(
          'This unit of work tag should not have side-effects. This error is ' +
            'likely caused by a bug in React. Please file an issue.',
        );
      }
    }
  }
}

Mutation 阶段的处理

beforeMutation 处理完成后,便会执行 Mutation 阶段的逻辑。在这个阶段 React 会将WorkInProgress 上的 Fiber 渲染在浏览器上。

beforeMutation类似, Mutatio的起点函数为commitMutationEffects

const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
  root,
  finishedWork,
);
// The next phase is the mutation phase, where we mutate the host tree.
commitMutationEffects(root, finishedWork, lanes);

commitMutationEffects 会深度遍历完成 begincomplete 阶段。

export function commitMutationEffects(
  root: FiberRoot,
  firstChild: Fiber,
  committedLanes: Lanes,
) {
  inProgressLanes = committedLanes;
  inProgressRoot = root;
  nextEffect = firstChild;
  commitMutationEffects_begin(root);
  inProgressLanes = null;
  inProgressRoot = null;
}

function commitMutationEffects_begin(root: FiberRoot) {
  while (nextEffect !== null) {
    const fiber = nextEffect;
    // TODO: Should wrap this in flags check, too, as optimization
    const deletions = fiber.deletions;
    if (deletions !== null) {
      for (let i = 0; i < deletions.length; i++) {
        const childToDelete = deletions[i];
        commitDeletion(root, childToDelete, fiber);
      }
    }
    const child = fiber.child;
    if ((fiber.subtreeFlags & MutationMask) !== NoFlags && child !== null) {
      nextEffect = child;
    } else {
      commitMutationEffects_complete(root);
    }
  }
}

function commitMutationEffects_complete(root: FiberRoot) {
  while (nextEffect !== null) {
    const fiber = nextEffect;
    commitMutationEffectsOnFiber(fiber, root);
    const sibling = fiber.sibling;
    if (sibling !== null) {
      nextEffect = sibling;
      return;
    }
    nextEffect = fiber.return;
  }
}

begin 阶段 : 在 render 阶段中,Fiber的部分 Child_Fiber 被打上delete的标签,对应的 DOM 节点在此刻被删除。如果当前FiberChild_Fiber节点不存在PlacementUpdateChildDeletionContentResetflag时,执行 complete 阶段。

complete 阶段: complete阶段会执行commitMutationEffectsOnFiber处理FibercommitMutationEffectsOnFiber 阶段根据flag进行不同的处理。如 Placement会将Fiber对应的DOM渲染在浏览器上。Update会更新对应的DOM节点。

function commitMutationEffectsOnFiber(finishedWork: Fiber, root: FiberRoot) {
  const flags = finishedWork.flags;
  const primaryFlags = flags & (Placement | Update | Hydrating);
  outer: switch (primaryFlags) {
    case Placement: {
      commitPlacement(finishedWork);
      finishedWork.flags &= ~Placement;
      break;
    }
    case PlacementAndUpdate: {
      // Placement
      commitPlacement(finishedWork);
      finishedWork.flags &= ~Placement;
      // Update
      const current = finishedWork.alternate;
      commitWork(current, finishedWork);
      break;
    }
    case Update: {
      const current = finishedWork.alternate;
      commitWork(current, finishedWork);
      break;
    }
  }
}

上面的commitWork逻辑执行了useLayoutEffectdestory 函数(即 useLayoutEffect的返回值)。逻辑如下,执行commitHookEffectListUnmount 处理所有 EffectTag 为 HookLayout | HookHasEffect 的Fiber , 执行他的 destory 函数。

这里出现了一个全新的HookInsertion|HookHasEffect 标识 ,根据源码猜测可能是一个新的hooks useInsertionEffect

function commitWork(current: Fiber | null, finishedWork: Fiber): void {
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case MemoComponent:
    case SimpleMemoComponent: {
      commitHookEffectListUnmount(
        HookInsertion | HookHasEffect,
        finishedWork,
        finishedWork.return,
      );
      commitHookEffectListMount(HookInsertion | HookHasEffect, finishedWork);
      commitHookEffectListUnmount(
        HookLayout | HookHasEffect,
        finishedWork,
        finishedWork.return,
      );
      return;
    }
      
    case ClassComponent: {
      return;
    }
      
    case HostComponent: {
      const instance: Instance = finishedWork.stateNode;
      if (instance != null) {
        // Commit the work prepared earlier.
        const newProps = finishedWork.memoizedProps;
        // For hydration we reuse the update path but we treat the oldProps
        // as the newProps. The updatePayload will contain the real change in
        // this case.
        const oldProps = current !== null ? current.memoizedProps : newProps;
        const type = finishedWork.type;
        // TODO: Type the updateQueue to be specific to host components.
        const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);
        finishedWork.updateQueue = null;
        if (updatePayload !== null) {
          commitUpdate(
            instance,
            updatePayload,
            type,
            oldProps,
            newProps,
            finishedWork,
          );
        }
      }
      return;
    }

    case HostText: {
      const textInstance: TextInstance = finishedWork.stateNode;
      const newText: string = finishedWork.memoizedProps;
      // For hydration we reuse the update path but we treat the oldProps
      // as the newProps. The updatePayload will contain the real change in
      // this case.
      const oldText: string =
        current !== null ? current.memoizedProps : newText;
      commitTextUpdate(textInstance, oldText, newText);
      return;
    }  
  }
}


function commitHookEffectListUnmount(
  flags: HookFlags,
  finishedWork: Fiber,
  nearestMountedAncestor: Fiber | null,
) {
 	const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      if ((effect.tag & flags) === flags) {
        // Unmount
        const destroy = effect.destroy;
        effect.destroy = undefined;
        if (destroy !== undefined) {
          safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
        }
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

Layout 阶段的处理

Layout 阶段发生于浏览器渲染完成之后,主要功能包括 componentDidMount ,useLayoutEffectcreate函数的执行。还有useEffectcreate/destory 异步函数的挂载。

layout 阶段的逻辑开始于 commitLayoutEffects, 在 处理 layout 阶段的逻辑之前,将 root 的 current 指向 finishedWork(即workInProgress)

// The work-in-progress tree is now the current tree. This must come after
// the mutation phase, so that the previous tree is still current during
// componentWillUnmount, but before the layout phase, so that the finished
// work is current during componentDidMount/Update.
root.current = finishedWork;
commitLayoutEffects(finishedWork, root, lanes);

类似的, commitLayoutEffects 存在 begincomplete 两个阶段。

begin 阶段: 无特殊逻辑

complete 阶段: 执行 commitLayoutEffectOnFiber 逻辑

额外提一下 OffscreenComponent 这个 flag,这会是一个新的API,将来会用于实现类似于 Vue 中 keep-alive功能。

export function commitLayoutEffects(
  finishedWork: Fiber,
  root: FiberRoot,
  committedLanes: Lanes,
): void {
  commitLayoutEffects_begin(finishedWork, root, committedLanes);
}

function commitLayoutEffects_begin(
  subtreeRoot: Fiber,
  root: FiberRoot,
  committedLanes: Lanes,
) {
  // Suspense layout effects semantics don't change for legacy roots.
  const isModernRoot = (subtreeRoot.mode & ConcurrentMode) !== NoMode;

  while (nextEffect !== null) {
    const fiber = nextEffect;
    const firstChild = fiber.child;

    if (
      enableSuspenseLayoutEffectSemantics &&
      fiber.tag === OffscreenComponent &&
      isModernRoot
    ) {
        ...
      }
        
    if ((fiber.subtreeFlags & LayoutMask) !== NoFlags && firstChild !== null) {
      ensureCorrectReturnPointer(firstChild, fiber);
      nextEffect = firstChild;
    } else {
      commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes);
    }
  }
}

function commitLayoutMountEffects_complete(
  subtreeRoot: Fiber,
  root: FiberRoot,
  committedLanes: Lanes,
) {
  while (nextEffect !== null) {
    const fiber = nextEffect;
    if ((fiber.flags & LayoutMask) !== NoFlags) {
      const current = fiber.alternate;
      commitLayoutEffectOnFiber(root, current, fiber, committedLanes);
    if (fiber === subtreeRoot) {
      nextEffect = null;
      return;
    }
    const sibling = fiber.sibling;
    if (sibling !== null) {
      nextEffect = sibling;
      return;
    }
    nextEffect = fiber.return;
  }
}

commitLayoutEffectOnFiber 中执行了 Class Component 中的 componentDidMount/componentDidUpdate 函数和 Function ComponentuseLayoutEffectcreate函数。代码如下

function commitLayoutEffectOnFiber(
  finishedRoot: FiberRoot,
  current: Fiber | null,
  finishedWork: Fiber,
  committedLanes: Lanes,
): void {
  if ((finishedWork.flags & LayoutMask) !== NoFlags) {
    switch (finishedWork.tag) {
      case FunctionComponent:
      case ForwardRef:
      case SimpleMemoComponent: {
        // 执行 函数组件的 useLayoutEffect 
        commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
        break;
      }
      case ClassComponent: 
        const instance = finishedWork.stateNode;
        if (finishedWork.flags & Update) {
          if (!offscreenSubtreeWasHidden) {
            if (current === null) {
              // mount 阶段执行 componentDidMount
              instance.componentDidMount();
            } else {
              const prevProps =
                finishedWork.elementType === finishedWork.type
                  ? current.memoizedProps
                  : resolveDefaultProps(
                      finishedWork.type,
                      current.memoizedProps,
                    );
              const prevState = current.memoizedState;
              // We could update instance props and state here,
              // but instead we rely on them being set during last render.
              // TODO: revisit this when we implement resuming.
                instance.componentDidUpdate(
                  prevProps,
                  prevState,
                  instance.__reactInternalSnapshotBeforeUpdate,
                )
          }
        }

        const updateQueue: UpdateQueue<
          *,
        > | null = (finishedWork.updateQueue: any);
          // 执行 setState 的第二个参数
          commitUpdateQueue(finishedWork, updateQueue, instance);
        }
        break;
      }
      case HostRoot: {
        // 执行 render 的第二个参数
        const updateQueue: UpdateQueue<
          *,
        > | null = (finishedWork.updateQueue: any);
        if (updateQueue !== null) {
          let instance = null;
          if (finishedWork.child !== null) {
            switch (finishedWork.child.tag) {
              case HostComponent:
                instance = getPublicInstance(finishedWork.child.stateNode);
                break;
              case ClassComponent:
                instance = finishedWork.child.stateNode;
                break;
            }
          }
          commitUpdateQueue(finishedWork, updateQueue, instance);
        }
        break;
      }
}

往期回顾

  1. React 原理分析(一) —— React 设计思想
  2. React 源码分析(二)—— Fiber 的 render 阶段

参考文章

  1. juejin.cn/post/691962…
  2. juejin.cn/post/691779…
  3. react.iamkasong.com/