Skip to content

5.3 Commit 阶段:副作用的执行

经过 Render 阶段的精心计算,React 已经构建了一棵 work-in-progress Fiber 树,并在这棵树上标记了所有需要执行的变更,我们称之为“副作用”(Side Effects)。现在,是时候将这些虚拟的变更应用到真实世界了。这个过程就发生在 Commit 阶段。

Commit 阶段是 React 更新流程的最后一步,它的入口函数是 commitRoot。与可中断的 Render 阶段不同,Commit 阶段是完全同步、不可中断的。这是为了确保 DOM 的一致性,防止用户看到渲染不完整的、破碎的 UI。

commitRoot 的工作可以被清晰地划分为三个子阶段,每个阶段都有明确的职责。

javascript
// react/packages/react-reconciler/src/ReactFiberWorkLoop.js

function commitRoot(
  root: FiberRoot,
  finishedWork: Fiber,
  // ...其他参数
) {
  const subtreeHasEffects = (finishedWork.subtreeFlags & MutationMask) !== NoFlags;
  const rootHasEffect = (finishedWork.flags & MutationMask) !== NoFlags;

  if (rootHasEffect || subtreeHasEffects) {
    // 1. Before Mutation 阶段
    // 遍历 Fiber 树,调用 getSnapshotBeforeUpdate
    commitBeforeMutationEffects(root, finishedWork);

    // 2. Mutation 阶段
    // 执行 DOM 的插入、更新、删除操作
    commitMutationEffects(root, finishedWork, lanes);

    // 将 current 指针指向新的 WIP 树,完成切换
    root.current = finishedWork;

    // 3. Layout 阶段
    // 调用 componentDidMount, componentDidUpdate 和 useLayoutEffect
    commitLayoutEffects(root, finishedWork, lanes);
  } else {
    // 如果没有副作用,直接完成切换
    root.current = finishedWork;
  }

  // ...后续处理,如调度 Passive Effects
}

1. Before Mutation 阶段

目标:在 DOM 发生实际变更之前,让组件有机会从 DOM 中读取一些信息。

这个阶段由 commitBeforeMutationEffects 函数负责。它会遍历 finishedWork 树,专门寻找带有 Snapshot 副作用标记的 ClassComponent。对于找到的每个组件,它会调用其 getSnapshotBeforeUpdate 生命周期方法。

javascript
// react/packages/react-reconciler/src/ReactFiberCommitWork.js

function commitBeforeMutationEffects(root: FiberRoot, firstChild: Fiber) {
  let fiber = firstChild;
  while (fiber !== null) {
    if (fiber.flags & Snapshot) {
      const current = fiber.alternate;
      commitBeforeMutationEffectOnFiber(current, fiber);
    }
    // ...遍历子节点和兄弟节点
  }
}

function commitBeforeMutationEffectOnFiber(current, finishedWork) {
  switch (finishedWork.tag) {
    case ClassComponent:
      const instance = finishedWork.stateNode;
      if (finishedWork.flags & Update) {
        const prevProps = finishedWork.memoizedProps;
        const prevState = finishedWork.memoizedState;
        instance.getSnapshotBeforeUpdate(prevProps, prevState);
      }
      break;
    // ...其他 case
  }
}

设计思考:为什么需要这个阶段?一个经典的案例是聊天应用的滚动条。在接收到新消息并渲染到列表之前,你可能需要记录下当前用户的滚动位置,以便在列表更新后恢复,避免新消息的插入导致页面“跳动”。getSnapshotBeforeUpdate 返回的值会作为 componentDidUpdate 的第三个参数,完美地解决了这个问题。

2. Mutation 阶段

目标:执行所有 DOM 的增、删、改操作。

这是 Commit 阶段最核心的部分,由 commitMutationEffects 函数执行。它会遍历副作用列表(一个在 Render 阶段构建的、只包含有副作用的 Fiber 的链表),并根据副作用的类型(Placement, Update, Deletion)执行相应的 DOM 操作。

  • Placement:插入新的 DOM 节点。React 会调用 parent.insertBefore()parent.appendChild()
  • Update:更新 DOM 节点的属性、样式或文本内容。
  • Deletion:从 DOM 中移除一个节点。React 会调用 parent.removeChild()
javascript
// react/packages/react-reconciler/src/ReactFiberCommitWork.js

function commitMutationEffects(root: FiberRoot, finishedWork: Fiber, lanes: Lanes) {
  // 遍历副作用列表并执行
  commitMutationEffectsOnFiber(finishedWork, root, lanes);
}

function commitMutationEffectsOnFiber(finishedWork, root, lanes) {
  const flags = finishedWork.flags;

  if (flags & Deletion) {
    // 执行删除操作
    commitDeletion(root, finishedWork, ...);
  }

  if (flags & Placement) {
    // 执行插入操作
    commitPlacement(finishedWork);
  }

  if (flags & Update) {
    // 执行更新操作
    commitWork(current, finishedWork);
  }
  // ...
}

这个阶段是唯一会实际修改宿主环境(如 DOM)的部分。为了效率,React 会在 Render 阶段收集所有变更,然后在 Mutation 阶段一次性批量执行。

3. Layout 阶段

目标:在 DOM 更新完成后,执行需要访问 DOM 布局信息的代码,并调用相应的生命周期方法。

当 Mutation 阶段完成,新的 UI 已经真实地呈现在屏幕上。此时,commitLayoutEffects 函数开始工作。它会再次遍历 Fiber 树,执行以下任务:

  1. 调用 componentDidMount(对于新挂载的类组件)。
  2. 调用 componentDidUpdate(对于更新的类组件)。
  3. 同步执行 useLayoutEffect 的回调函数。
javascript
// react/packages/react-reconciler/src/ReactFiberCommitWork.js

function commitLayoutEffects(root: FiberRoot, finishedWork: Fiber, lanes: Lanes) {
  // 遍历 Fiber 树并执行 Layout Effects
  commitLayoutEffects_begin(finishedWork, root, lanes);
}

function commitLayoutEffectOnFiber(finishedWork: Fiber, root: FiberRoot, lanes: Lanes) {
  const flags = finishedWork.flags;
  if (flags & (Update | Callback)) {
    switch (finishedWork.tag) {
      case ClassComponent:
        const instance = finishedWork.stateNode;
        if (finishedWork.flags & Update) {
          instance.componentDidUpdate(...);
        }
        break;
      case FunctionComponent:
        // 执行 useLayoutEffect 的回调
        commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
        break;
    }
  }
}

设计思考useLayoutEffectuseEffect 的关键区别就在于执行时机。useLayoutEffect 在浏览器绘制之前同步执行,因此它适合执行那些需要读取或修改 DOM 布局并希望用户看不到中间状态的操作(例如,测量元素尺寸并设置样式)。但因为它会阻塞浏览器绘制,所以应谨慎使用,优先选择 useEffect

useEffect 的异步调度

你可能已经注意到,useEffect 并没有在上述三个阶段中被调用。这是因为 useEffect 被设计为异步执行,以避免阻塞浏览器渲染。在 commitRoot 的末尾,如果检测到有 PassiveuseEffect 的内部代号)副作用,React 会使用调度器(Scheduler)调度一个低优先级的任务,在未来的某个时间点异步地执行 useEffect 的回调和清理函数。

这个设计确保了即使 useEffect 中有耗时操作,也不会影响到本次渲染的绘制,从而提升了用户感知的性能。

至此,一次完整的 React 更新流程——从 setState 到屏幕更新——就全部完成了。

Last updated: