Skip to content

Before Mutation Effects 阶段

在这个阶段,React主要做了以下的事情:

  1. 捕获 DOM 快照: 最典型的用例是类组件的 getSnapshotBeforeUpdate() 生命周期方法。它允许组件在 DOM 更新前捕获当前 DOM 的某些状态(例如滚动位置、元素大小等)。
  2. 执行卸载相关的清理 (部分): 虽然组件的完全卸载横跨多个子阶段,但 componentWillUnmount 的调用也发生在这个“突变前”的窗口期,确保在组件的 DOM 节点被移除前执行清理逻辑。

虽然 React 更推崇使用 Hooks,但 getSnapshotBeforeUpdate 仍然是处理特定场景(如维护滚动位置或焦点)的有效工具,尤其是在与第三方库或旧有代码集成时。

对于函数组件,虽然没有直接等同于 getSnapshotBeforeUpdate 的 Hook 在“突变前”执行任意代码,但 useLayoutEffect 的清理函数(如果组件更新或卸载)会在 DOM 突变后、浏览器绘制前同步执行,而其创建函数也是同步的。不过,严格意义上的“突变前读取”主要还是 getSnapshotBeforeUpdate 的领域。

函数调用链路图

函数源码解析

commitBeforeMutationEffects

commitBeforeMutationEffects
javascript
// ... existing code ...
export function commitBeforeMutationEffects(
  root: FiberRoot,
  firstChild: Fiber,
  committedLanes: Lanes,
): void {
  focusedInstanceHandle = prepareForCommit(root.containerInfo); // 准备提交,例如在RN中获取焦点信息
  shouldFireAfterActiveInstanceBlur = false;

  const isViewTransitionEligible =
    enableViewTransition &&
    includesOnlyViewTransitionEligibleLanes(committedLanes);

  nextEffect = firstChild; // 从第一个子节点开始遍历
  commitBeforeMutationEffects_begin(isViewTransitionEligible);

  focusedInstanceHandle = null;
  resetAppearingViewTransitions();
}

function commitBeforeMutationEffects_begin(isViewTransitionEligible: boolean) {
  const subtreeMask = isViewTransitionEligible
    ? BeforeAndAfterMutationTransitionMask // 包含 Snapshot 和 ViewTransition 相关的 flags
    : BeforeMutationMask; // 通常只包含 Snapshot flag
  while (nextEffect !== null) {
    const fiber = nextEffect;
    const child = fiber.child;
    // 检查当前 Fiber 的 subtreeFlags 是否包含需要处理的副作用
    if ((fiber.subtreeFlags & subtreeMask) !== NoFlags && child !== null) {
      // ... 确保父节点存在 ...
      child.return = fiber;
      nextEffect = child; // 向下遍历
    } else {
      commitBeforeMutationEffects_complete(isViewTransitionEligible);
    }
  }
}

function commitBeforeMutationEffects_complete(isViewTransitionEligible: boolean) {
  while (nextEffect !== null) {
    const fiber = nextEffect;
    // 实际处理当前 Fiber 的 BeforeMutation Effects
    commitBeforeMutationEffectsOnFiber(fiber, isViewTransitionEligible);
    const sibling = fiber.sibling;
    if (sibling !== null) {
      // ... 确保父节点存在 ...
      sibling.return = fiber.return;
      nextEffect = sibling; // 处理兄弟节点
      return;
    }
  }
}

// 真正执行单个 Fiber 的 BeforeMutation Effect 的函数
function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber, isViewTransitionEligible: boolean) {
  const current = finishedWork.alternate;
  const flags = finishedWork.flags;

  if ((flags & Snapshot) !== NoFlags) { // 检查 Snapshot 标记
    switch (finishedWork.tag) {
      case ClassComponent:
        if (current !== null) {
          // 调用 getSnapshotBeforeUpdate
          commitClassSnapshot(current, finishedWork);
        }
        break;
      case HostRoot:
        // ... HostRoot 的 Snapshot 处理 (例如 Hydration) ...
        break;
      // ... 其他类型的 Fiber (HostComponent, HostText 等通常不处理 Snapshot) ...
    }
  }
  if (enableViewTransition && isViewTransitionEligible) {
    if ((flags & ViewTransitionNamedStatic) !== NoFlags) {
      // 视图过渡相关的处理
      commitBeforeUpdateViewTransition(finishedWork);
    }
  }
  // ...
}
// ... existing code ...

这个函数是 React “变更前副作用”(Before Mutation Effects)阶段的起点。它的主要工作流程是:

  1. 准备工作:

    • 调用 prepareForCommit 来获取当前宿主环境的一些状态(比如浏览器中当前聚焦的元素),并存储在 focusedInstanceHandle 中。
    • 判断当前提交是否适用于“视图过渡”(View Transition)特性,结果保存在 isViewTransitionEligible
  2. 启动遍历:

    • 将全局变量 nextEffect 指向 firstChild,即副作用链表中的第一个 Fiber 节点。
    • 调用 commitBeforeMutationEffects_begin(isViewTransitionEligible) 来开始遍历和处理副作用链表。这个 _begin 函数会进行深度优先遍历,并在遍历过程中,当一个节点的子树处理完毕后,会调用 commitBeforeMutationEffects_complete,而 _complete 函数内部则会调用 commitBeforeMutationEffectsOnFiber 来处理当前节点。
  3. 收尾工作:

    • 在遍历完成后,重置 focusedInstanceHandle 和视图过渡相关的状态。

简单来说,commitBeforeMutationEffects 设置好初始状态,然后通过调用内部的辅助函数(如 commitBeforeMutationEffects_begincommitBeforeMutationEffects_complete)来驱动一个深度优先的副作用处理流程。在这个流程中,commitBeforeMutationEffectsOnFiber 会被适时调用。

commitBeforeMutationEffectsOnFiber

当遍历到副作用链表中的某个具体 Fiber 节点 (finishedWork) 时,此函数会被调用来执行该节点在“变更前”阶段需要处理的特定副作用。

主要逻辑是根据 finishedWork.tag (Fiber 类型) 和 finishedWork.flags (副作用标记) 进行 switch 判断:

  • 对于 ClassComponent:

    • 如果该 Fiber 带有 Snapshot 标记 (通常意味着组件定义了 getSnapshotBeforeUpdate 生命周期方法并且需要执行它):
      • 会调用 commitClassSnapshot(finishedWork, current)。这个函数内部会获取组件实例,并调用其实例的 getSnapshotBeforeUpdate 方法,将返回的快照值存储起来,供后续的 componentDidUpdate 使用。
  • 对于 HostRoot (根节点):

    • 如果根 Fiber 带有 Snapshot 标记 (比较少见的情况,例如整个应用因 key 变化而重新挂载):
      • 并且当前环境支持 DOM 操作 (supportsMutation 为 true),则会调用 clearContainer(root.containerInfo) 来清空根 DOM 容器的内容。
  • 对于 FunctionComponent (当 enableUseEffectEventHook 特性开启时):

    • 如果 Fiber 有 Update 标记,并且其 updateQueue 中有 useEffectEvent 产生的事件回调,则会更新这些事件回调的实现,确保它们是最新的。
  • 对于 ViewTransitionComponent (当 enableViewTransition 特性开启时):

    • 如果符合视图过渡条件,并且是更新操作 (current !== null),会调用 commitBeforeUpdateViewTransition 来为即将发生的视图过渡做准备,比如记录旧的视图名称。
  • 其他类型的 Fiber (如 HostComponent, HostText 等) 在此阶段通常没有特别的 Snapshot 副作用需要处理。如果它们错误地带上了 Snapshot 标记,会抛出错误。

此外,在 switch 之前,还会处理与焦点管理相关的逻辑:

  • 如果之前记录的 focusedInstanceHandle (当前聚焦的元素) 位于一个即将被隐藏的 SuspenseComponent 内部,会设置 shouldFireAfterActiveInstanceBlur = true 并调用 beforeActiveInstanceBlur,为后续在 Mutation 阶段结束后处理失焦事件做准备。

总而言之,commitBeforeMutationEffects 负责驱动整个“变更前副作用”的遍历,而 commitBeforeMutationEffectsOnFiber 是实际执行单个 Fiber 节点特定副作用(主要是 getSnapshotBeforeUpdate 的调用、焦点处理和视图过渡准备)的核心函数。


微信公众号二维码