Skip to content

commitBeforeMutationEffects

Commit 阶段对于副作用的第二次遍历处理过程。

前提条件

在这个阶段每一个 FiberNode 对应的 DOM 元素已经生成,但是没有插入到浏览器中(保存在内存中),

作用

其主要作用如下:

  1. 处理节点的 Blur 事件
  • 待删除节点 Blur 事件

  • Suspense 组件从 visible 切换到 hide 的时候,会触发子节点的 Blur 事件

  1. 处理 Class 组件 生命周期方法副作用

具体流程如下:

image-20250526165740210

源码阅读

1. 获取 focus 节点

js
export function prepareForCommit(containerInfo: Container): Object | null {
  // 判断是否允许事件处理
  eventsEnabled = ReactBrowserEventEmitterIsEnabled();
  // 获取当前获得焦点的元素对象
  selectionInformation = getSelectionInformation();
  let activeInstance = null;
  if (enableCreateEventHandleAPI) {
    // 获取当前获得焦点的元素对象
    const focusedElem = selectionInformation.focusedElem;
    // 如果存在选中的 或者 激活的节点
    if (focusedElem !== null) {
      // 获取其对应的Fiber节点(当前不存在就查找最近的祖先节点)
      activeInstance = getClosestInstanceFromNode(focusedElem);
    }
  }
  ReactBrowserEventEmitterSetEnabled(false);
  return activeInstance;
}

其主要通过 document.activeElement 来获取当前浏览器中激活的元素节点,然后通过缓存在节点本身 或者 最近祖先节点中的 Fiber 对象 获取其对应的 Fiber 对象。

2. 处理节点的 Blur 事件

  1. 待删除节点 Blur 事件 在深度遍历的过程中, 如果发现当前节点中存在 deletions 数组,说明当前节点需要被删除, 那么这时候就需要判断待删除的节点是否包含 focusedElem节点, 如果包含了, 那么说明节点需要触发 Blur 事件了。

  2. Suspense 组件从 visible 切换到 hide 的时候,会触发子节点的 Blur 事件

除了节点被移除的情况下会默认触发 Blur 事件外,对于那些更新的节点,其也有可能触发 Blur 事件, 如 Suspense组件。

因为组件本身在 hide 状态下,其隐藏的节点不是物理上的移除,仍然经历 Render 阶段的,但是在浏览器的 DOM 树中其是被删除的,这种不统一的情况,通过deletions 数组是无法知道其是否需要触发 Blur 事件, 所以在满足以下条件下的 Suspense 组件也触发对应的 Blur 事件

  • 本身节点是 Suspense 组件
  • 本身的状态得从 visible 切换到 hidden状态
  • focusedElem节点 是其子节点

具体的判断条件如下:

js
if (
  finishedWork.tag === SuspenseComponent &&
  isSuspenseBoundaryBeingHidden(current, finishedWork) &&
  doesFiberContain(finishedWork, focusedInstanceHandle)
) {
  shouldFireAfterActiveInstanceBlur = true;
  beforeActiveInstanceBlur(finishedWork);
}

处理 Class 组件 getSnapshotBeforeUpdate() 生命周期方法副作用

这个过程具体的方法为 commitBeforeMutationLifeCycles(current, finishedWork )

js
/**
 * Commit阶段 在DOM插入浏览器之前 触发生命周期函数
 *  - 回调 Class组件 getSnapshotBeforeUpdate() 生命周期方法副作用
 *  - HostRoot 清除根节点的子元素 div#root 下的元素
 * @param {*} current
 * @param {*} finishedWork
 * @returns
 */
function commitBeforeMutationLifeCycles(
  current: Fiber | null,
  finishedWork: Fiber
): void {
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent:
    case Block: {
      return;
    }
    case ClassComponent: {
      if (finishedWork.flags & Snapshot) {
        if (current !== null) {
          const prevProps = current.memoizedProps;
          const prevState = current.memoizedState;
          const instance = finishedWork.stateNode;
          // 触发 getSnapshotBeforeUpdate(prevProps, prevState)
          // prevProps 之前的props
          // 之前的 state
          const snapshot = instance.getSnapshotBeforeUpdate(
            finishedWork.elementType === finishedWork.type
              ? prevProps
              : resolveDefaultProps(finishedWork.type, prevProps),
            prevState
          );

          instance.__reactInternalSnapshotBeforeUpdate = snapshot;
        }
      }
      return;
    }
    case HostRoot: {
      if (supportsMutation) {
        if (finishedWork.flags & Snapshot) {
          const root = finishedWork.stateNode;
          // 清空跟节点DOM元素div#root下的子元素
          clearContainer(root.containerInfo);
        }
      }
      return;
    }
    case HostComponent:
    case HostText:
    case HostPortal:
    case IncompleteClassComponent:
      // Nothing to do for these component types
      return;
  }
}

可以看到其核心就是调用 class 组件的 getSnapshotBeforeUpdate 生命周期方法

getSnapshotBeforeUpdate()

在最近一次渲染输出(提交到 DOM 节点)之前调用。它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。此生命周期方法的任何返回值将作为参数传递给 componentDidUpdate()

总结

  1. 为什么需要在commitBeforeMutationEffects 处理 Blur 事件?

因为 Blur 事件的触发既有可能是对于更新后仍然存在的节点,但是更大可能是对于那些更新后删除的节点。 commitBeforeMutationEffects 是 DOM 更新到浏览器之前的最后一个阶段,所以可以在这个阶段中处理 Blur 事件。

  1. 为什么需要在commitBeforeMutationEffects 处理 Class 组件的 getSnapshotBeforeUpdate() 生命周期方法?

仍然是因为commitBeforeMutationEffects 是 DOM 更新到浏览器之前的最后一个阶段,所以可以在这个阶段中处理 Class 组件的 getSnapshotBeforeUpdate() 生命周期方法。

处理 useEffect 类型的 effect 副作用

image

这是官网对于 useEffect 的解释,具体功能放到 useEffect 讲解

我们这边主要说的是在这个过程中,会将 useEffect 类型的副作用放到任务队列中去

js
// 处理 useEffect 类型的 effect
// 主要是将其放到任务队列中去
if ((flags & Passive) !== NoFlags) {
  // If there are passive effects, schedule a callback to flush at
  // the earliest opportunity.
  if (!rootDoesHavePassiveEffects) {
    rootDoesHavePassiveEffects = true;
    scheduleCallback(NormalSchedulerPriority, () => {
      flushPassiveEffects();
      return null;
    });
  }
}