Skip to content

第12章 提交阶段

本章将深入React的commit阶段,理解如何将render阶段计算出的变更应用到真实DOM,以及Effect的执行时机和顺序。这是React渲染流程的最后一个关键阶段。

在前面的章节中,我们学习了React的协调过程(Reconciliation)。render阶段通过Diff算法找出了需要执行的DOM操作,并标记在Fiber节点的flags上。但是,这些操作还没有真正应用到DOM上。

这就是commit阶段的职责。commit阶段负责将render阶段的计算结果应用到真实DOM,执行副作用(Effect),更新ref,调用生命周期方法等。

为什么React要将渲染分为render和commit两个阶段?commit阶段为什么不能中断?before mutation、mutation、layout三个子阶段分别做什么?useEffect和useLayoutEffect有什么区别?

本章将逐一解答这些问题,带你深入理解React commit阶段的设计与实现。


12.1 commit阶段概述

commit阶段是React渲染流程的最后一个阶段,它将render阶段计算出的变更应用到真实DOM。

12.1.1 commitRoot入口

当render阶段完成后,React会调用commitRoot函数进入commit阶段:

javascript
// 文件:packages/react-reconciler/src/ReactFiberWorkLoop.js
// 行号:2100-2150(React 19.3.0)

function commitRoot(
  root: FiberRoot,
  recoverableErrors: null | Array<CapturedValue<mixed>>,
) {
  // 1. 获取当前的优先级
  const previousUpdateLanePriority = getCurrentUpdatePriority();
  const prevTransition = ReactCurrentBatchConfig.transition;
  
  try {
    // 2. 设置为最高优先级(DiscreteEventPriority)
    ReactCurrentBatchConfig.transition = null;
    setCurrentUpdatePriority(DiscreteEventPriority);
    
    // 3. 执行commit工作
    commitRootImpl(
      root,
      recoverableErrors,
      previousUpdateLanePriority,
    );
  } finally {
    // 4. 恢复优先级
    ReactCurrentBatchConfig.transition = prevTransition;
    setCurrentUpdatePriority(previousUpdateLanePriority);
  }
  
  return null;
}

为什么commit阶段使用最高优先级?

commit阶段必须同步执行,不能被打断。因为:

  1. DOM操作的原子性:部分更新会导致页面状态不一致
  2. 用户体验:避免用户看到中间状态
  3. 副作用的顺序:Effect必须按照特定顺序执行

12.1.2 三个子阶段

commit阶段分为三个子阶段,每个子阶段处理不同类型的副作用:

javascript
// 文件:packages/react-reconciler/src/ReactFiberWorkLoop.js
// 行号:2200-2400(React 19.3.0)

function commitRootImpl(
  root: FiberRoot,
  recoverableErrors: null | Array<CapturedValue<mixed>>,
  renderPriorityLevel: EventPriority,
) {
  // 获取finishedWork(render阶段完成的Fiber树)
  const finishedWork = root.finishedWork;
  const lanes = root.finishedLanes;
  
  if (finishedWork === null) {
    return null;
  }
  
  // 清空finishedWork和finishedLanes
  root.finishedWork = null;
  root.finishedLanes = NoLanes;
  
  // 保存当前的Fiber树
  root.callbackNode = null;
  root.callbackPriority = NoLane;
  
  // ===== 第一阶段:before mutation =====
  // 在DOM变更之前执行
  const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
    root,
    finishedWork,
  );
  
  // ===== 第二阶段:mutation =====
  // 执行DOM变更
  commitMutationEffects(root, finishedWork, lanes);
  
  // 切换current树
  root.current = finishedWork;
  
  // ===== 第三阶段:layout =====
  // 在DOM变更之后执行
  commitLayoutEffects(finishedWork, root, lanes);
  
  // 请求绘制
  requestPaint();
  
  // 调度useEffect
  if (
    (finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
    (finishedWork.flags & PassiveMask) !== NoFlags
  ) {
    if (!rootDoesHavePassiveEffects) {
      rootDoesHavePassiveEffects = true;
      scheduleCallback(NormalSchedulerPriority, () => {
        flushPassiveEffects();
        return null;
      });
    }
  }
  
  // 检查是否还有其他工作
  ensureRootIsScheduled(root);
  
  return null;
}

三个子阶段的职责

阶段时机主要工作
before mutationDOM变更前getSnapshotBeforeUpdate、调度useEffect
mutationDOM变更中插入、更新、删除DOM节点
layoutDOM变更后useLayoutEffect、ref更新、生命周期

为什么需要三个子阶段?

  1. before mutation:在DOM变更前获取快照

    • 例如:获取滚动位置、焦点状态
    • 调度useEffect(异步执行)
  2. mutation:执行DOM操作

    • 这是唯一修改DOM的阶段
    • 操作完成后切换current树
  3. layout:在DOM变更后执行同步副作用

    • useLayoutEffect可以读取最新的DOM
    • ref可以获取最新的DOM引用
    • 生命周期方法可以访问最新的DOM

current树的切换时机

注意,current树的切换发生在mutation阶段之后、layout阶段之前:

before mutation阶段:
- root.current指向旧的Fiber树
- 可以访问旧的DOM状态

mutation阶段:
- root.current仍指向旧的Fiber树
- 正在修改DOM

切换current树:
- root.current = finishedWork
- 现在指向新的Fiber树

layout阶段:
- root.current指向新的Fiber树
- 可以访问新的DOM状态

这样设计的好处:

  • before mutation阶段可以访问旧的DOM状态
  • layout阶段可以访问新的DOM状态
  • 保证了状态的一致性

12.2 before mutation阶段

before mutation阶段在DOM变更之前执行,主要负责调用getSnapshotBeforeUpdate生命周期和调度useEffect

12.2.1 commitBeforeMutationEffects

javascript
// 文件:packages/react-reconciler/src/ReactFiberCommitWork.js
// 行号:300-400(React 19.3.0)

export function commitBeforeMutationEffects(
  root: FiberRoot,
  firstChild: Fiber,
): boolean {
  focusedInstanceHandle = prepareForCommit(root.containerInfo);
  
  nextEffect = firstChild;
  commitBeforeMutationEffects_begin();
  
  // 返回是否需要在mutation后触发blur事件
  const shouldFire = shouldFireAfterActiveInstanceBlur;
  shouldFireAfterActiveInstanceBlur = false;
  focusedInstanceHandle = null;
  
  return shouldFire;
}

function commitBeforeMutationEffects_begin() {
  while (nextEffect !== null) {
    const fiber = nextEffect;
    
    // 处理子节点
    const child = fiber.child;
    if (
      (fiber.subtreeFlags & BeforeMutationMask) !== NoFlags &&
      child !== null
    ) {
      child.return = fiber;
      nextEffect = child;
    } else {
      commitBeforeMutationEffects_complete();
    }
  }
}

function commitBeforeMutationEffects_complete() {
  while (nextEffect !== null) {
    const fiber = nextEffect;
    
    try {
      commitBeforeMutationEffectsOnFiber(fiber);
    } catch (error) {
      captureCommitPhaseError(fiber, fiber.return, error);
    }
    
    const sibling = fiber.sibling;
    if (sibling !== null) {
      sibling.return = fiber.return;
      nextEffect = sibling;
      return;
    }
    
    nextEffect = fiber.return;
  }
}

遍历模式

before mutation阶段使用深度优先遍历,但只处理有BeforeMutationMask flags的节点:

Fiber树:
        App
       /   \
    Header  Main (有Snapshot flag)
     /       /  \
  Logo    List  Sidebar
          /  \
       Item1 Item2

遍历过程:
1. App:检查subtreeFlags,有BeforeMutationMask,向下
2. Header:检查subtreeFlags,没有BeforeMutationMask,跳过子树
3. Main:有Snapshot flag,执行commitBeforeMutationEffectsOnFiber
4. 继续遍历其他节点...

12.2.2 调度useEffect

before mutation阶段的一个重要工作是调度useEffect

javascript
// 文件:packages/react-reconciler/src/ReactFiberCommitWork.js
// 行号:500-600(React 19.3.0)

function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
  const current = finishedWork.alternate;
  const flags = finishedWork.flags;
  
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent: {
      // 处理useEffect
      if ((flags & Passive) !== NoFlags) {
        // 标记有Passive effect需要执行
        if (!rootDoesHavePassiveEffects) {
          rootDoesHavePassiveEffects = true;
        }
      }
      break;
    }
    case ClassComponent: {
      if ((flags & Snapshot) !== NoFlags) {
        if (current !== null) {
          // 调用getSnapshotBeforeUpdate
          const prevProps = current.memoizedProps;
          const prevState = current.memoizedState;
          const instance = finishedWork.stateNode;
          
          const snapshot = instance.getSnapshotBeforeUpdate(
            finishedWork.elementType === finishedWork.type
              ? prevProps
              : resolveDefaultProps(finishedWork.type, prevProps),
            prevState,
          );
          
          // 保存snapshot,供componentDidUpdate使用
          instance.__reactInternalSnapshotBeforeUpdate = snapshot;
        }
      }
      break;
    }
    case HostRoot: {
      if ((flags & Snapshot) !== NoFlags) {
        // 清空容器(首次渲染时)
        const root = finishedWork.stateNode;
        clearContainer(root.containerInfo);
      }
      break;
    }
  }
}

getSnapshotBeforeUpdate的使用场景

getSnapshotBeforeUpdate在DOM变更前被调用,可以获取变更前的DOM状态:

jsx
class ChatThread extends React.Component {
  chatThreadRef = React.createRef();
  
  getSnapshotBeforeUpdate(prevProps, prevState) {
    // 在新消息添加前,保存滚动位置
    if (prevProps.messages.length < this.props.messages.length) {
      const chat = this.chatThreadRef.current;
      return chat.scrollHeight - chat.scrollTop;
    }
    return null;
  }
  
  componentDidUpdate(prevProps, prevState, snapshot) {
    // 如果有新消息,保持滚动位置
    if (snapshot !== null) {
      const chat = this.chatThreadRef.current;
      chat.scrollTop = chat.scrollHeight - snapshot;
    }
  }
  
  render() {
    return (
      <div ref={this.chatThreadRef}>
        {this.props.messages.map(msg => (
          <div key={msg.id}>{msg.text}</div>
        ))}
      </div>
    );
  }
}

为什么在before mutation阶段调度useEffect?

useEffect是异步执行的,需要在commit阶段开始时就调度,这样可以:

  1. 尽早调度:不用等到layout阶段
  2. 不阻塞渲染:useEffect异步执行,不会阻塞浏览器绘制
  3. 保证顺序:在mutation和layout之前调度,保证执行顺序

12.3 mutation阶段

mutation阶段是commit阶段的核心,负责执行所有的DOM操作。

12.3.1 Placement插入操作

Placement flag表示需要插入新的DOM节点:

javascript
// 文件:packages/react-reconciler/src/ReactFiberCommitWork.js
// 行号:1500-1600(React 19.3.0)

function commitPlacement(finishedWork: Fiber): void {
  // 1. 找到父DOM节点
  const parentFiber = getHostParentFiber(finishedWork);
  
  let parent;
  let isContainer;
  const parentStateNode = parentFiber.stateNode;
  
  switch (parentFiber.tag) {
    case HostComponent:
      parent = parentStateNode;
      isContainer = false;
      break;
    case HostRoot:
      parent = parentStateNode.containerInfo;
      isContainer = true;
      break;
    case HostPortal:
      parent = parentStateNode.containerInfo;
      isContainer = true;
      break;
    default:
      throw new Error('Invalid host parent fiber.');
  }
  
  // 2. 如果父节点有ContentReset flag,清空文本内容
  if (parentFiber.flags & ContentReset) {
    resetTextContent(parent);
    parentFiber.flags &= ~ContentReset;
  }
  
  // 3. 找到插入位置(before节点)
  const before = getHostSibling(finishedWork);
  
  // 4. 执行插入操作
  if (isContainer) {
    insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
  } else {
    insertOrAppendPlacementNode(finishedWork, before, parent);
  }
}

getHostSibling(查找插入位置)

这是commit阶段最复杂的函数之一,用于找到正确的插入位置:

javascript
// 文件:packages/react-reconciler/src/ReactFiberCommitWork.js
// 行号:1200-1400(React 19.3.0)

function getHostSibling(fiber: Fiber): Instance | null {
  // 查找下一个Host类型的兄弟节点
  
  let node: Fiber = fiber;
  siblings: while (true) {
    // 向上查找,直到找到有兄弟节点的节点
    while (node.sibling === null) {
      if (node.return === null || isHostParent(node.return)) {
        // 没有兄弟节点
        return null;
      }
      node = node.return;
    }
    
    node.sibling.return = node.return;
    node = node.sibling;
    
    // 向下查找第一个Host类型的节点
    while (node.tag !== HostComponent && node.tag !== HostText) {
      // 如果这个节点也是新插入的,跳过
      if (node.flags & Placement) {
        continue siblings;
      }
      
      // 如果没有子节点,继续查找兄弟节点
      if (node.child === null) {
        continue siblings;
      } else {
        node.child.return = node;
        node = node.child;
      }
    }
    
    // 找到了Host节点,且不是新插入的
    if (!(node.flags & Placement)) {
      return node.stateNode;
    }
  }
}

为什么查找插入位置这么复杂?

因为Fiber树和DOM树的结构不一致:

jsx
// React组件树
<div>
  <List>
    <Item />  {/* 新插入 */}
  </List>
  <span>text</span>
</div>

// Fiber树
div
├── List (FunctionComponent)
│   └── Item (HostComponent) [Placement]
└── span (HostComponent)

// DOM树
div
├── Item (需要插入到span前面)
└── span

getHostSibling需要跳过组件节点,找到真正的DOM兄弟节点。

insertOrAppendPlacementNode(执行插入)

javascript
// 文件:packages/react-reconciler/src/ReactFiberCommitWork.js

function insertOrAppendPlacementNode(
  node: Fiber,
  before: Instance | null,
  parent: Instance,
): void {
  const tag = node.tag;
  const isHost = tag === HostComponent || tag === HostText;
  
  if (isHost) {
    // 是Host节点,直接插入
    const stateNode = node.stateNode;
    if (before) {
      insertBefore(parent, stateNode, before);
    } else {
      appendChild(parent, stateNode);
    }
  } else {
    // 不是Host节点,递归处理子节点
    const child = node.child;
    if (child !== null) {
      insertOrAppendPlacementNode(child, before, parent);
      let sibling = child.sibling;
      while (sibling !== null) {
        insertOrAppendPlacementNode(sibling, before, parent);
        sibling = sibling.sibling;
      }
    }
  }
}

12.3.2 Update更新操作

Update flag表示需要更新DOM节点的属性:

javascript
// 文件:packages/react-reconciler/src/ReactFiberCommitWork.js
// 行号:1700-1800(React 19.3.0)

function commitUpdate(
  domElement: Instance,
  updatePayload: Array<mixed>,
  type: string,
  oldProps: Props,
  newProps: Props,
  internalInstanceHandle: Object,
): void {
  // 更新Fiber引用
  updateFiberProps(domElement, newProps);
  
  // 更新DOM属性
  updateProperties(domElement, updatePayload, type, oldProps, newProps);
}

updateProperties(更新DOM属性)

javascript
// 文件:packages/react-dom-bindings/src/client/ReactDOMComponent.js

export function updateProperties(
  domElement: Element,
  updatePayload: Array<any>,
  tag: string,
  lastProps: Object,
  nextProps: Object,
): void {
  // updatePayload格式:[key1, value1, key2, value2, ...]
  
  for (let i = 0; i < updatePayload.length; i += 2) {
    const propKey = updatePayload[i];
    const propValue = updatePayload[i + 1];
    
    if (propKey === 'style') {
      // 更新style
      setValueForStyles(domElement, propValue);
    } else if (propKey === 'dangerouslySetInnerHTML') {
      // 更新innerHTML
      setInnerHTML(domElement, propValue);
    } else if (propKey === 'children') {
      // 更新文本内容
      setTextContent(domElement, propValue);
    } else {
      // 更新其他属性
      setValueForProperty(domElement, propKey, propValue);
    }
  }
  
  // 处理特殊标签的后续逻辑
  switch (tag) {
    case 'input':
      // 更新input的value
      updateInput(domElement, nextProps);
      break;
    case 'textarea':
      // 更新textarea的value
      updateTextarea(domElement, nextProps);
      break;
    case 'select':
      // 更新select的value
      updateSelect(domElement, nextProps);
      break;
  }
}

示例:更新DOM属性

jsx
// 旧props
<div className="old" style={{ color: 'red' }}>
  Old Text
</div>

// 新props
<div className="new" style={{ color: 'blue', fontSize: '14px' }}>
  New Text
</div>

// updatePayload
[
  'className', 'new',
  'style', { color: 'blue', fontSize: '14px' },
  'children', 'New Text',
]

// 执行更新
domElement.className = 'new';
domElement.style.color = 'blue';
domElement.style.fontSize = '14px';
domElement.textContent = 'New Text';

12.3.3 Deletion删除操作

Deletion flag表示需要删除DOM节点:

javascript
// 文件:packages/react-reconciler/src/ReactFiberCommitWork.js
// 行号:1900-2100(React 19.3.0)

function commitDeletion(
  finishedRoot: FiberRoot,
  current: Fiber,
  nearestMountedAncestor: Fiber,
): void {
  // 1. 卸载ref
  // 2. 调用componentWillUnmount
  // 3. 执行useEffect cleanup
  // 4. 删除DOM节点
  
  unmountHostComponents(finishedRoot, current, nearestMountedAncestor);
}

function unmountHostComponents(
  finishedRoot: FiberRoot,
  current: Fiber,
  nearestMountedAncestor: Fiber,
): void {
  let node: Fiber = current;
  
  // 深度优先遍历,卸载所有节点
  let currentParentIsValid = false;
  let currentParent;
  let currentParentIsContainer;
  
  while (true) {
    if (!currentParentIsValid) {
      // 查找父DOM节点
      let parent = node.return;
      findParent: while (true) {
        const parentStateNode = parent.stateNode;
        switch (parent.tag) {
          case HostComponent:
            currentParent = parentStateNode;
            currentParentIsContainer = false;
            break findParent;
          case HostRoot:
            currentParent = parentStateNode.containerInfo;
            currentParentIsContainer = true;
            break findParent;
          case HostPortal:
            currentParent = parentStateNode.containerInfo;
            currentParentIsContainer = true;
            break findParent;
        }
        parent = parent.return;
      }
      currentParentIsValid = true;
    }
    
    if (node.tag === HostComponent || node.tag === HostText) {
      // 递归卸载子节点
      commitNestedUnmounts(finishedRoot, node, nearestMountedAncestor);
      
      // 删除DOM节点
      if (currentParentIsContainer) {
        removeChildFromContainer(currentParent, node.stateNode);
      } else {
        removeChild(currentParent, node.stateNode);
      }
    } else {
      // 处理其他类型的节点
      commitUnmount(finishedRoot, node, nearestMountedAncestor);
      
      if (node.child !== null) {
        node.child.return = node;
        node = node.child;
        continue;
      }
    }
    
    if (node === current) {
      return;
    }
    
    while (node.sibling === null) {
      if (node.return === null || node.return === current) {
        return;
      }
      node = node.return;
    }
    
    node.sibling.return = node.return;
    node = node.sibling;
  }
}

commitUnmount(卸载节点)

javascript
// 文件:packages/react-reconciler/src/ReactFiberCommitWork.js

function commitUnmount(
  finishedRoot: FiberRoot,
  current: Fiber,
  nearestMountedAncestor: Fiber,
): void {
  switch (current.tag) {
    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent: {
      // 执行useEffect cleanup
      const updateQueue = current.updateQueue;
      if (updateQueue !== null) {
        const lastEffect = updateQueue.lastEffect;
        if (lastEffect !== null) {
          const firstEffect = lastEffect.next;
          let effect = firstEffect;
          do {
            const destroy = effect.destroy;
            if (destroy !== undefined) {
              safelyCallDestroy(current, nearestMountedAncestor, destroy);
            }
            effect = effect.next;
          } while (effect !== firstEffect);
        }
      }
      break;
    }
    case ClassComponent: {
      // 卸载ref
      safelyDetachRef(current, nearestMountedAncestor);
      
      // 调用componentWillUnmount
      const instance = current.stateNode;
      if (typeof instance.componentWillUnmount === 'function') {
        safelyCallComponentWillUnmount(
          current,
          nearestMountedAncestor,
          instance,
        );
      }
      break;
    }
    case HostComponent: {
      // 卸载ref
      safelyDetachRef(current, nearestMountedAncestor);
      break;
    }
  }
}

删除操作的顺序

删除节点时,需要按照特定的顺序执行:

1. 递归处理子节点
   - 执行useEffect cleanup
   - 调用componentWillUnmount
   - 卸载ref

2. 删除DOM节点
   - 从父节点移除

3. 清理Fiber节点
   - 断开引用
   - 释放内存

示例:

jsx
function Parent() {
  const [show, setShow] = useState(true);
  
  return (
    <div>
      <button onClick={() => setShow(false)}>Hide</button>
      {show && <Child />}
    </div>
  );
}

function Child() {
  useEffect(() => {
    console.log('Child mounted');
    return () => {
      console.log('Child cleanup'); // 先执行
    };
  }, []);
  
  return <div>Child</div>; // 后删除DOM
}

// 点击Hide按钮时的执行顺序:
// 1. Child cleanup(useEffect cleanup)
// 2. 删除<div>Child</div>(DOM删除)

12.4 layout阶段

layout阶段在DOM变更之后执行,主要负责执行useLayoutEffect、更新ref、调用生命周期方法。

12.4.1 useLayoutEffect执行

javascript
// 文件:packages/react-reconciler/src/ReactFiberCommitWork.js
// 行号:400-500(React 19.3.0)

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

function commitLayoutEffects_begin(
  subtreeRoot: Fiber,
  root: FiberRoot,
  committedLanes: Lanes,
) {
  while (nextEffect !== null) {
    const fiber = nextEffect;
    const firstChild = fiber.child;
    
    if ((fiber.subtreeFlags & LayoutMask) !== NoFlags && firstChild !== null) {
      firstChild.return = 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;
      try {
        commitLayoutEffectOnFiber(root, current, fiber, committedLanes);
      } catch (error) {
        captureCommitPhaseError(fiber, fiber.return, error);
      }
    }
    
    if (fiber === subtreeRoot) {
      nextEffect = null;
      return;
    }
    
    const sibling = fiber.sibling;
    if (sibling !== null) {
      sibling.return = fiber.return;
      nextEffect = sibling;
      return;
    }
    
    nextEffect = fiber.return;
  }
}

commitLayoutEffectOnFiber(执行layout副作用)

javascript
// 文件:packages/react-reconciler/src/ReactFiberCommitWork.js

function commitLayoutEffectOnFiber(
  finishedRoot: FiberRoot,
  current: Fiber | null,
  finishedWork: Fiber,
  committedLanes: Lanes,
): void {
  const flags = finishedWork.flags;
  
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent: {
      // 执行useLayoutEffect
      if ((flags & (Update | Callback)) !== NoFlags) {
        commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
      }
      break;
    }
    case ClassComponent: {
      const instance = finishedWork.stateNode;
      if (flags & Update) {
        if (current === null) {
          // 首次渲染:调用componentDidMount
          instance.componentDidMount();
        } else {
          // 更新:调用componentDidUpdate
          const prevProps =
            finishedWork.elementType === finishedWork.type
              ? current.memoizedProps
              : resolveDefaultProps(finishedWork.type, current.memoizedProps);
          const prevState = current.memoizedState;
          
          // 获取snapshot(来自getSnapshotBeforeUpdate)
          const snapshot = instance.__reactInternalSnapshotBeforeUpdate;
          
          instance.componentDidUpdate(prevProps, prevState, snapshot);
        }
      }
      
      // 执行setState的callback
      if (flags & Callback) {
        const updateQueue = finishedWork.updateQueue;
        if (updateQueue !== null) {
          commitUpdateQueue(finishedWork, updateQueue, instance);
        }
      }
      break;
    }
    case HostRoot: {
      // 执行ReactDOM.render的callback
      if (flags & Callback) {
        const updateQueue = finishedWork.updateQueue;
        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;
    }
    case HostComponent: {
      // 处理autoFocus
      const instance = finishedWork.stateNode;
      if (current === null && flags & Update) {
        const type = finishedWork.type;
        const props = finishedWork.memoizedProps;
        commitMount(instance, type, props, finishedWork);
      }
      break;
    }
  }
}

commitHookEffectListMount(执行useLayoutEffect)

javascript
// 文件:packages/react-reconciler/src/ReactFiberCommitWork.js

function commitHookEffectListMount(
  flags: HookFlags,
  finishedWork: Fiber,
) {
  const updateQueue = finishedWork.updateQueue;
  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  
  if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      if ((effect.tag & flags) === flags) {
        // 执行useLayoutEffect的create函数
        const create = effect.create;
        effect.destroy = create();
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

useLayoutEffect vs useEffect

jsx
function Component() {
  const [count, setCount] = useState(0);
  
  useLayoutEffect(() => {
    console.log('useLayoutEffect', count);
    return () => {
      console.log('useLayoutEffect cleanup', count);
    };
  }, [count]);
  
  useEffect(() => {
    console.log('useEffect', count);
    return () => {
      console.log('useEffect cleanup', count);
    };
  }, [count]);
  
  return <div>{count}</div>;
}

// 执行顺序:
// 1. render阶段
// 2. before mutation阶段(调度useEffect)
// 3. mutation阶段(更新DOM)
// 4. layout阶段
//    - useLayoutEffect cleanup(同步)
//    - useLayoutEffect(同步)
// 5. commit阶段完成
// 6. 浏览器绘制
// 7. useEffect cleanup(异步)
// 8. useEffect(异步)
特性useLayoutEffectuseEffect
执行时机DOM变更后,浏览器绘制前浏览器绘制后
是否阻塞渲染是(同步)否(异步)
适用场景需要读取DOM布局、同步更新DOM数据获取、订阅、日志
性能影响可能阻塞渲染不阻塞渲染

useLayoutEffect的使用场景

jsx
// 场景1:测量DOM尺寸
function Tooltip() {
  const [tooltipHeight, setTooltipHeight] = useState(0);
  const ref = useRef(null);
  
  useLayoutEffect(() => {
    // 在浏览器绘制前测量高度,避免闪烁
    const height = ref.current.getBoundingClientRect().height;
    setTooltipHeight(height);
  }, []);
  
  return <div ref={ref} style={{ top: -tooltipHeight }}>Tooltip</div>;
}

// 场景2:同步更新DOM
function ScrollToTop() {
  useLayoutEffect(() => {
    // 在浏览器绘制前滚动到顶部,避免看到滚动过程
    window.scrollTo(0, 0);
  }, []);
  
  return <div>Content</div>;
}

12.4.2 ref的绑定

layout阶段还负责更新ref:

javascript
// 文件:packages/react-reconciler/src/ReactFiberCommitWork.js

function commitAttachRef(finishedWork: Fiber) {
  const ref = finishedWork.ref;
  if (ref !== null) {
    const instance = finishedWork.stateNode;
    let instanceToUse;
    
    switch (finishedWork.tag) {
      case HostComponent:
        // DOM节点
        instanceToUse = getPublicInstance(instance);
        break;
      default:
        // 类组件实例
        instanceToUse = instance;
    }
    
    if (typeof ref === 'function') {
      // ref callback
      ref(instanceToUse);
    } else {
      // ref object
      ref.current = instanceToUse;
    }
  }
}

function commitDetachRef(current: Fiber) {
  const currentRef = current.ref;
  if (currentRef !== null) {
    if (typeof currentRef === 'function') {
      // ref callback
      currentRef(null);
    } else {
      // ref object
      currentRef.current = null;
    }
  }
}

ref的更新时机

jsx
function Component() {
  const ref = useRef(null);
  
  useLayoutEffect(() => {
    // 此时ref.current已经指向DOM节点
    console.log(ref.current); // <div>Content</div>
  }, []);
  
  useEffect(() => {
    // 此时ref.current也已经指向DOM节点
    console.log(ref.current); // <div>Content</div>
  }, []);
  
  return <div ref={ref}>Content</div>;
}

// 执行顺序:
// 1. mutation阶段:创建/更新DOM
// 2. layout阶段:
//    - 更新ref(ref.current = DOM节点)
//    - 执行useLayoutEffect(可以访问ref.current)
// 3. 浏览器绘制
// 4. 执行useEffect(可以访问ref.current)

ref callback的使用

jsx
function MeasureExample() {
  const [height, setHeight] = useState(0);
  
  const measuredRef = useCallback(node => {
    if (node !== null) {
      // ref callback在layout阶段执行
      setHeight(node.getBoundingClientRect().height);
    }
  }, []);
  
  return (
    <>
      <h1 ref={measuredRef}>Hello, world</h1>
      <h2>The above header is {Math.round(height)}px tall</h2>
    </>
  );
}

12.5 Effect的异步调度

useEffect是异步执行的,在commit阶段完成后才会执行。

12.5.1 flushPassiveEffects

在before mutation阶段,React会调度flushPassiveEffects

javascript
// 文件:packages/react-reconciler/src/ReactFiberWorkLoop.js
// 行号:2500-2600(React 19.3.0)

function flushPassiveEffects(): boolean {
  if (rootWithPendingPassiveEffects !== null) {
    const root = rootWithPendingPassiveEffects;
    const lanes = pendingPassiveEffectsLanes;
    
    rootWithPendingPassiveEffects = null;
    pendingPassiveEffectsLanes = NoLanes;
    
    // 执行useEffect
    return flushPassiveEffectsImpl();
  }
  return false;
}

function flushPassiveEffectsImpl() {
  if (rootWithPendingPassiveEffects === null) {
    return false;
  }
  
  const root = rootWithPendingPassiveEffects;
  rootWithPendingPassiveEffects = null;
  
  // 1. 执行所有useEffect的cleanup函数
  commitPassiveUnmountEffects(root.current);
  
  // 2. 执行所有useEffect的create函数
  commitPassiveMountEffects(root, root.current);
  
  return true;
}

commitPassiveUnmountEffects(执行cleanup)

javascript
// 文件:packages/react-reconciler/src/ReactFiberCommitWork.js

function commitPassiveUnmountEffects(finishedWork: Fiber): void {
  setCurrentDebugFiberInDEV(finishedWork);
  commitPassiveUnmountEffects_begin();
  setCurrentDebugFiberInDEV(null);
}

function commitPassiveUnmountEffects_begin() {
  while (nextEffect !== null) {
    const fiber = nextEffect;
    const child = fiber.child;
    
    if ((fiber.subtreeFlags & PassiveMask) !== NoFlags && child !== null) {
      child.return = 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) {
      sibling.return = fiber.return;
      nextEffect = sibling;
      return;
    }
    
    nextEffect = fiber.return;
  }
}

function commitPassiveUnmountOnFiber(finishedWork: Fiber): void {
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent: {
      // 执行useEffect cleanup
      commitHookEffectListUnmount(
        HookPassive | HookHasEffect,
        finishedWork,
        finishedWork.return,
      );
      break;
    }
  }
}

commitPassiveMountEffects(执行create)

javascript
// 文件:packages/react-reconciler/src/ReactFiberCommitWork.js

function commitPassiveMountEffects(
  root: FiberRoot,
  finishedWork: Fiber,
): void {
  setCurrentDebugFiberInDEV(finishedWork);
  commitPassiveMountEffects_begin(finishedWork, root);
  setCurrentDebugFiberInDEV(null);
}

function commitPassiveMountEffects_begin(
  subtreeRoot: Fiber,
  root: FiberRoot,
) {
  while (nextEffect !== null) {
    const fiber = nextEffect;
    const firstChild = fiber.child;
    
    if ((fiber.subtreeFlags & PassiveMask) !== NoFlags && firstChild !== null) {
      firstChild.return = fiber;
      nextEffect = firstChild;
    } else {
      commitPassiveMountEffects_complete(subtreeRoot, root);
    }
  }
}

function commitPassiveMountEffects_complete(
  subtreeRoot: Fiber,
  root: FiberRoot,
) {
  while (nextEffect !== null) {
    const fiber = nextEffect;
    
    if ((fiber.flags & Passive) !== NoFlags) {
      try {
        commitPassiveMountOnFiber(root, fiber);
      } catch (error) {
        captureCommitPhaseError(fiber, fiber.return, error);
      }
    }
    
    if (fiber === subtreeRoot) {
      nextEffect = null;
      return;
    }
    
    const sibling = fiber.sibling;
    if (sibling !== null) {
      sibling.return = fiber.return;
      nextEffect = sibling;
      return;
    }
    
    nextEffect = fiber.return;
  }
}

function commitPassiveMountOnFiber(
  finishedRoot: FiberRoot,
  finishedWork: Fiber,
): void {
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case SimpleMemoComponent: {
      // 执行useEffect create
      commitHookEffectListMount(HookPassive | HookHasEffect, finishedWork);
      break;
    }
  }
}

12.5.2 cleanup函数的调用

cleanup函数的调用时机有两种:

  1. 组件更新时:先执行旧的cleanup,再执行新的create
  2. 组件卸载时:执行cleanup
jsx
function Component({ id }) {
  useEffect(() => {
    console.log('Effect create', id);
    return () => {
      console.log('Effect cleanup', id);
    };
  }, [id]);
  
  return <div>{id}</div>;
}

// 首次渲染 id=1
// 输出:Effect create 1

// 更新 id=2
// 输出:
// Effect cleanup 1  (先执行旧的cleanup)
// Effect create 2   (再执行新的create)

// 卸载
// 输出:Effect cleanup 2

cleanup函数的执行顺序

jsx
function Parent() {
  useEffect(() => {
    console.log('Parent effect');
    return () => console.log('Parent cleanup');
  }, []);
  
  return <Child />;
}

function Child() {
  useEffect(() => {
    console.log('Child effect');
    return () => console.log('Child cleanup');
  }, []);
  
  return <div>Child</div>;
}

// 首次渲染:
// Child effect
// Parent effect

// 卸载:
// Child cleanup
// Parent cleanup

cleanup函数按照深度优先的顺序执行,先执行子组件的cleanup,再执行父组件的cleanup。

为什么useEffect是异步的?

useEffect异步执行有几个重要原因:

  1. 不阻塞渲染:让浏览器尽快绘制,提高用户体验
  2. 批量执行:可以将多个Effect合并执行
  3. 避免阻塞交互:用户可以立即与页面交互
jsx
function SlowEffect() {
  useEffect(() => {
    // 耗时操作
    for (let i = 0; i < 1000000000; i++) {}
    console.log('Effect done');
  }, []);
  
  return <div>Content</div>;
}

// 如果useEffect是同步的:
// 1. render阶段
// 2. commit阶段
// 3. 执行useEffect(阻塞1秒)
// 4. 浏览器绘制
// 结果:用户看到白屏1秒

// useEffect是异步的:
// 1. render阶段
// 2. commit阶段
// 3. 浏览器绘制(用户立即看到内容)
// 4. 执行useEffect(不阻塞)
// 结果:用户立即看到内容

useEffect的调度优先级

useEffect使用NormalSchedulerPriority调度,优先级低于用户交互:

javascript
// 文件:packages/react-reconciler/src/ReactFiberWorkLoop.js

if (
  (finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
  (finishedWork.flags & PassiveMask) !== NoFlags
) {
  if (!rootDoesHavePassiveEffects) {
    rootDoesHavePassiveEffects = true;
    // 使用NormalSchedulerPriority调度
    scheduleCallback(NormalSchedulerPriority, () => {
      flushPassiveEffects();
      return null;
    });
  }
}

这意味着:

  • 如果有高优先级更新(如用户输入),会先处理高优先级更新
  • useEffect可能会被延迟执行
  • 保证了用户交互的流畅性

示例:Effect的完整生命周期

jsx
function Counter() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    console.log('Effect: count =', count);
    
    const timer = setInterval(() => {
      console.log('Timer: count =', count);
    }, 1000);
    
    return () => {
      console.log('Cleanup: count =', count);
      clearInterval(timer);
    };
  }, [count]);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

// 首次渲染(count=0):
// Effect: count = 0
// Timer: count = 0(每秒输出)

// 点击按钮(count=1):
// Cleanup: count = 0(先清理旧的Effect)
// Effect: count = 1(再执行新的Effect)
// Timer: count = 1(每秒输出)

// 卸载:
// Cleanup: count = 1

本章小结

本章深入讲解了React的commit阶段,这是React渲染流程的最后一个关键阶段。让我们回顾一下关键要点:

commit阶段的三个子阶段

  1. before mutation阶段:DOM变更前

    • 调用getSnapshotBeforeUpdate
    • 调度useEffect(异步执行)
    • 准备DOM变更
  2. mutation阶段:执行DOM操作

    • Placement:插入新节点
    • Update:更新节点属性
    • Deletion:删除节点
    • 切换current树
  3. layout阶段:DOM变更后

    • 执行useLayoutEffect(同步)
    • 更新ref
    • 调用生命周期方法

Effect的执行时机

Effect类型执行阶段是否同步适用场景
useLayoutEffectlayout阶段同步读取DOM布局、同步更新DOM
useEffectcommit后异步异步数据获取、订阅、日志
componentDidMountlayout阶段同步类组件初始化
componentDidUpdatelayout阶段同步类组件更新

关键设计原则

  1. commit阶段不可中断:保证DOM操作的原子性
  2. Effect分离:同步Effect(useLayoutEffect)和异步Effect(useEffect)
  3. 优先级保证:commit阶段使用最高优先级
  4. 顺序保证:cleanup先于create执行

性能优化建议

  1. 优先使用useEffect:除非必须同步读取DOM
  2. 避免在useLayoutEffect中执行耗时操作:会阻塞渲染
  3. 正确清理Effect:避免内存泄漏
  4. 合理使用依赖数组:避免不必要的Effect执行

思考题

  1. 为什么current树的切换发生在mutation和layout之间?
  2. useLayoutEffect和useEffect的cleanup函数执行顺序有什么区别?
  3. 如果在useLayoutEffect中触发setState,会发生什么?
  4. 为什么删除节点时要先执行cleanup,再删除DOM?

在下一章中,我们将学习React的事件系统,理解合成事件的工作原理和事件优先级。