大家好,我是前端西瓜哥。今天我们来详细讲解一下 React 的 commit 阶段的逻辑。

创新互联公司主营农安网站建设的网络公司,主营网站建设方案,app开发定制,农安h5微信平台小程序开发搭建,农安网站营销推广欢迎农安等地区企业咨询
React 版本为 18.2.0
commitRootImpl 中的三个函数的调用分别对应这个三个阶段:
function commitRootImpl(){
  // BeforeMutation 阶段
  commitBeforeMutationEffects(root, finishedWork);
  // Mutation 阶段
  commitMutationEffects(root, finishedWork, lanes);
  // Layout 阶段
  commitLayoutEffects(finishedWork, root, lanes);
}在 reconcil (调和)阶段,给 fiber 打了很多的 flags(标记),commit 阶段是会读取这些 flags 进行不同的操作的。
flags 是通过二进制掩码的方式来保存的,掩码优点是节省内存,缺点是可读性很差。
使用或位运算,可以将多个 flag 组合成一个组。
我这三个阶段 用到的组合掩码 为:
export const BeforeMutationMask =
Update |
Snapshot;
export const MutationMask =
Placement |
Update |
ChildDeletion |
ContentReset |
Ref |
Hydrating |
Visibility;
export const LayoutMask = Update | Callback | Ref | Visibility;
BeforeMutation 阶段。
commitRootImpl 首先会 调用 commitBeforeMutationEffects 方法。
commitBeforeMutationEffects 的核心实现:
function commitBeforeMutationEffects(root, firstChild) {
  nextEffect = firstChild;
  commitBeforeMutationEffects_begin();
}主要是调用这个 commitBeforeMutationEffects_begin 方法。
begin 干了啥?
进行深度优先遍历,找到最后一个带有 BeforeMutation 标识的 fiber。这是因为 useEffect 的调用逻辑是从子到父,要找最后一个 fiber 作为起点。
commitBeforeMutationEffects_begin 的核心实现:
function commitBeforeMutationEffects_begin() {
  while (nextEffect !== null) {
    const fiber = nextEffect;
    // 取出子 fiber
    const child = fiber.child;
    if (
      (fiber.subtreeFlags & BeforeMutationMask) !== NoFlags &&
      child !== null
    ) {
      // 说明子 fiber 树中存在 BeforeMutationMask 标识的 fiber
      // 那就继续遍历往下找
      child.return = fiber;
      nextEffect = child;
    } else {
      // 找不到了,说明到底了,执行 complete 逻辑。
      commitBeforeMutationEffects_complete();
    }
  }
}subtreeFlags 是当前 fiber 的子树的标识汇总,目的是防止无意义的完整深度遍历,能够更早地结束遍历。如果直接用 flags,是要遍历到叶子节点才能知道到底谁是要找的最有一个节点。
找到后,调用 complete 。
commitBeforeMutationEffects_complete 实现为:
function commitBeforeMutationEffects_complete() {
  while (nextEffect !== null) {
    const fiber = nextEffect;
    try {
      // BeforeMutation 阶段真正做的事情
      commitBeforeMutationEffectsOnFiber(fiber);
    } catch (error) {
      captureCommitPhaseError(fiber, fiber.return, error);
    }
    const sibling = fiber.sibling;
    if (sibling !== null) { // 没有下一个兄弟节点
      sibling.return = fiber.return;
      nextEffect = sibling;
      return;
      // 结束后会回到 begin 中的循环中
      // 继续往下找最后一个 带有 BeforeMutation 标识的 fiber
    }
  
    // 从上往下,处理
    nextEffect = fiber.return;
  }
}前面很多逻辑都是遍历的逻辑,真正的核心操作在 commitBeforeMutationEffectsOnFiber 方法。
做了什么?
对标记了 Snapshot 的组件进行处理,通常是类组件,会 调用类组件实例 instance 的 getSnapshotBeforeUpdate 方法,生成快照对象,然后再放到 instance.__reactInternalSnapshotBeforeUpdate 下,作为之后的 componentDidUpdate 钩子函数的第三个参数。
其他类型的组件基本啥都不做。
function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
  const current = finishedWork.alternate;
  const flags = finishedWork.flags;
  // flags 存在 Snapshot
  if ((flags & Snapshot) !== NoFlags) {
    switch (finishedWork.tag) {
      case FunctionComponent:
      case ForwardRef:
      case SimpleMemoComponent: {
        break;
      }
      // 类组件
      case ClassComponent: {
        if (current !== null) {
          const prevProps = current.memoizedProps;
          const prevState = current.memoizedState;
          const instance = finishedWork.stateNode;
          // 调用类组件实例的 getSnapshotBeforeUpdate 生成快照对象
          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;
      }
      case HostComponent:
      case HostText:
      case HostPortal:
      case IncompleteClassComponent:
        // 啥也不做
        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 阶段是最重要的阶段,在这个阶段,React 真正地更新了文档 DOM 树。
入口函数是 commitMutationEffects,但它只是 commitMutationEffectsOnFiber 的封装。
function commitMutationEffects(root, finishedWork, committedLanes) {
  inProgressLanes = committedLanes;
  inProgressRoot = root;
  commitMutationEffectsOnFiber(finishedWork, root, committedLanes);
  inProgressLanes = null;
  inProgressRoot = null;
}每个 fiber 都要传入 commitMutationEffectsOnFiber,执行 mutation 主逻辑。
从这调用栈可知 commitMutationEffectsOnFiber 递归调用了多次,形成了很长的调用栈。
commitMutationEffectsOnFiber 的核心实现为:
function commitMutationEffectsOnFiber(finishedWork, root, lanes) {
  const current = finishedWork.alternate;
  const flags = finishedWork.flags;
  
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case MemoComponent:
    case SimpleMemoComponent: {
      // 做了一些事情
      commitReconciliationEffects(finishedWork);
    }
    // 类组件
    case ClassComponent: {
      recursivelyTraverseMutationEffects(root, finishedWork, lanes);
      commitReconciliationEffects(finishedWork);
      if (flags & Ref) {
        if (current !== null) {
          safelyDetachRef(current, current.return);
        }
      }
      return;
    }
}对不同类型的 fiber 会进行不同的处理,但有一些公共逻辑会执行的,那就是:
// Deletion 深度遍历执行删除操作
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
// Placement 插入逻辑
commitReconciliationEffects(finishedWork);
首先是调用 recursivelyTraverseMutationEffects 方法,这个方法会执行删除逻辑。
该方法会读取 fiber 的 deletions 数组,对这些要删除的 fiber 进行操作。
function recursivelyTraverseMutationEffects(root, parentFiber, lanes) {
  // Deletions effects can be scheduled on any fiber type. They need to happen
  // before the children effects hae fired.
  const deletions = parentFiber.deletions;
  if (deletions !== null) {
    for (let i = 0; i < deletions.length; i++) {
      const childToDelete = deletions[i];
      try {
        // 执行 fiber 的删除逻辑
        commitDeletionEffects(root, parentFiber, childToDelete);
      } catch (error) {
        captureCommitPhaseError(childToDelete, parentFiber, error);
      }
    }
  }
  // 【下面的可不看】其实就是对子节点遍历,也执行 mutation 主逻辑。
  if (parentFiber.subtreeFlags & MutationMask) {
    let child = parentFiber.child;
    while (child !== null) {
      // 又调用 mutation 逻辑的入口函数
      commitMutationEffectsOnFiber(child, root, lanes);
      child = child.sibling;
    }
  }
}对于要删除的 fiber,我们这里讨论原生组件、类组件、函数组件这 3 种组件类型 fiber 的删除逻辑。
对于原生组件类型(div、span 这些):
function commitDeletionEffectsOnFiber(finishedRoot, nearestMountedAncestor, deletedFiber) {
  // deletedFiber 表示那个要被删除的 fiber
  switch (deletedFiber.tag) {
    /********* 原生组件 *********/
    case HostComponent: {
      if (!offscreenSubtreeWasHidden) {
        // ref 设置回 null
        safelyDetachRef(deletedFiber, nearestMountedAncestor);
      }
      // 后面的 HostText 会接着执行,switch 就是这个逻辑
    }
    case HostText: {
      const prevHostParent = hostParent;
      const prevHostParentIsContainer = hostParentIsContainer;
      hostParent = null;
 
      // 往下遍历子节点,执行删除
      recursivelyTraverseDeletionEffects(
        finishedRoot,
        nearestMountedAncestor,
        deletedFiber,
      );
 
      hostParent = prevHostParent;
      hostParentIsContainer = prevHostParentIsContainer;
      // 删除真正的 DOM,调用了原生的 removeChild 方法
      if (hostParent !== null) {
        if (hostParentIsContainer) {
          removeChildFromContainer(
            ((hostParent: any): Container),
            (deletedFiber.stateNode: Instance | TextInstance),
          );
        } else {
          removeChild(
            ((hostParent: any): Instance),
            (deletedFiber.stateNode: Instance | TextInstance),
          );
        }
      }
      return;
    }
    // 其他组件类型
}对于类组件:
function commitDeletionEffectsOnFiber(finishedRoot, nearestMountedAncestor, deletedFiber) {
  // deletedFiber 表示那个要被删除的 fiber
  switch (deletedFiber.tag) {
    // ...
      
    /********* 类组件 *********/
    case ClassComponent: {
      if (!offscreenSubtreeWasHidden) {
        // 移除 ref
        safelyDetachRef(deletedFiber, nearestMountedAncestor);
        const instance = deletedFiber.stateNode;
        if (typeof instance.componentWillUnmount === 'function') {
          // 调用类组件实例的 componentWillUnmount 方法
          safelyCallComponentWillUnmount(
            deletedFiber,
            nearestMountedAncestor,
            instance,
          );
        }
      }
      // 遍历子节点执行删除逻辑
      recursivelyTraverseDeletionEffects(
        finishedRoot,
        nearestMountedAncestor,
        deletedFiber,
      );
      return;
    }
    // ...
  }
}对于函数组件:
function commitDeletionEffectsOnFiber(finishedRoot, nearestMountedAncestor, deletedFiber) {
  // deletedFiber 表示那个要被删除的 fiber
  switch (deletedFiber.tag) {
    // ...
 
    /********* 函数组件 *********/
    case FunctionComponent:
    case ForwardRef:
    case MemoComponent:
    case SimpleMemoComponent: {
      if (!offscreenSubtreeWasHidden) {
        const updateQueue = deletedFiber.updateQueue;
        if (updateQueue !== null) {
          // 读取 updateQueue 队列,队列用链表的方式保存
          const lastEffect = updateQueue.lastEffect;
          if (lastEffect !== null) {
            const firstEffect = lastEffect.next;
            let effect = firstEffect;
            do {
              const {destroy, tag} = effect;
              if (destroy !== undefined) {
                // 处理 useInsertionEffect 产生的副作用
                // 执行 useInsertionEffect 回调函数返回的函数,即 destroy
                if ((tag & HookInsertion) !== NoHookEffect) {
                  // safelyCallDestroy 只是加了 try-catch 去调用 destroy
                  safelyCallDestroy(
                    deletedFiber,
                    nearestMountedAncestor,
                    destroy,
                  );
                // useLayoutEffect 同理
                } else if ((tag & HookLayout) !== NoHookEffect) {
                  safelyCallDestroy(
                    deletedFiber,
                    nearestMountedAncestor,
                    destroy,
                  );
                }
              }
              // 找下一个 effect
              effect = effect.next;
            } while (effect !== firstEffect);
          }
        }
      }
      // 向下递归
      recursivelyTraverseDeletionEffects(
        finishedRoot,
        nearestMountedAncestor,
        deletedFiber,
      );
      return;
    }
    default: {
      recursivelyTraverseDeletionEffects(
        finishedRoot,
        nearestMountedAncestor,
        deletedFiber,
      );
      return;
    }
  }
}完成删除逻辑后,接着就是调用 commitReconciliationEffects,这个方法负责往真实 DOM 树中插入 DOM 节点。
commitReconciliationEffects 核心内容:
function commitReconciliationEffects(finishedWork) {
  const flags = finishedWork.flags;
  if (flags & Placement) {
    try {
      // 执行 Placement 插入逻辑
      commitPlacement(finishedWork);
    } catch (error) {
      captureCommitPhaseError(finishedWork, finishedWork.return, error);
    }
    // 移除 Placement 标志
    finishedWork.flags &= ~Placement;
  }
  if (flags & Hydrating) {
    finishedWork.flags &= ~Hydrating;
  }
}如果 finishedWork 有 Placement 标识,则调用 commitPlacement 方法。
commitPlacement 的逻辑为:
commitPlacement 实现如下。
function commitPlacement(finishedWork) {
  // 获取父 fiber
  const parentFiber = getHostParentFiber(finishedWork);
  switch (parentFiber.tag) {
    case HostComponent: {
      const parent = parentFiber.stateNode;
      // 父 fiber 是否有 ContentReset(内容重置)标记
      if (parentFiber.flags & ContentReset) {
        // 其实就是 parent.textContent = '';
        resetTextContent(parent); // 
        // 移除 ContentReset 标志
        parentFiber.flags &= ~ContentReset;
      }
      // 找它的下一个兄弟 DOM 节点,后面用 insertBefore 方法
      // 如果没有,就调用原生的 appendChild 方法
      const before = getHostSibling(finishedWork);
      insertOrAppendPlacementNode(finishedWork, before, parent);
      break;
    }
    case HostRoot:
    case HostPortal: {
      const parent: Container = parentFiber.stateNode.containerInfo;
      const before = getHostSibling(finishedWork);
      insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
      break;
    }
    // eslint-disable-next-line-no-fallthrough
    default:
      throw new Error(
        'Invalid host parent fiber. This error is likely caused by a bug ' +
          'in React. Please file an issue.',
      );
  }
}Placement 只会在原生组件和 fiber 根节点上标记,没有函数组件和类组件什么事。
对于可复用的原生组件,会 调用 commitUpdate 进行更新。
commitUpdate 的代码:
function commitUpdate(domElement, updatePayload, type, oldProps, newProps) {
  // 对比更新,需要处理 onXx、className 这些特殊的 props
  updateProperties(domElement, updatePayload, type, oldProps, newProps);
  // 更新 DOM 元素的 "__reactProps$ + randomKey" 为这个新的 props
  updateFiberProps(domElement, newProps);
}类组件不会进行更新操作。
对于函数组件,会依次调用:
需要注意,函数组件初次挂载,flags 也会标记为 Update,走更新逻辑。这也是为什么 useEffect 在函数组件挂载时也会执行,和类组件的 componentDidUpate 不同。
// 找出 useInsertionEffect 的 destroy 方法去调用
// 需要注意 destroy 可能为 undefined(函数组件初次挂载的情况下)
commitHookEffectListUnmount(HookInsertion | HookHasEffect, finishedWork, finishedWork.return);
// 执行 useInsertionEffect 的回调函数,并将返回值保存到 effect.destory 里。
commitHookEffectListMount(HookInsertion | HookHasEffect, finishedWork);
// useLayoutEffect 对应的 destroy 方法
// 同样可能不存在
commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork, finishedWork.return);
最后是 Layout 阶段。
commitLayoutEffects 的实现:
function commitLayoutEffects(finishedWork, root, committedLanes) {
  inProgressLanes = committedLanes;
  inProgressRoot = root;
  nextEffect = finishedWork;
  // 又是 begin,和 BeforeMutation 阶段类似的递归逻辑
  commitLayoutEffects_begin(finishedWork, root, committedLanes);
  inProgressLanes = null;
  inProgressRoot = null;
}和 BeforeMutation 阶段一样,先深度优先递归,找最后一个有 LayoutMask 标记的 fiber。
然后从下往上调用 complete 逻辑,确保逻辑是从底部到顶部,即先子后父。
function commitLayoutEffects_begin(subtreeRoot, root, committedLanes) {
  // 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 (fiber.tag === OffscreenComponent) {
      // 离屏组件的逻辑,不讲
      continue;
    }
    // 找最后一个有 LayoutMask 标记的 fiber
    if ((fiber.subtreeFlags & LayoutMask) !== NoFlags && firstChild !== null) {
      firstChild.return = fiber;
      nextEffect = firstChild;
    } else {
      // 到底了,就执行 complete
      commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes);
    }
  }
}complete 代码:
function commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes) {
  while (nextEffect !== null) {
    const fiber = nextEffect;
    if ((fiber.flags & LayoutMask) !== NoFlags) {
      const current = fiber.alternate;
      try {
        // 调用 commitLayoutEffectOnFiber
        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 方法。它会根据组件类型不同执行不同逻辑。
对于函数组件,会调用 useLayoutEffect 的回调函数(effect.create)。
对于类组件:
处理完后,接下来就会 更新 ref :
if (finishedWork.flags & Ref) {
  commitAttachRef(finishedWork);
}操作很简单,对于原生组件,就是给 fiber.ref.current 赋值为 fiber.stateNode。
现在还差 useEffect 没调用了。
useEffect 不在同步的 commit 阶段中执行。它是异步的,被 scheduler 异步调度执行。
function commitRootImpl(){
  // 异步调度
  scheduleCallback(NormalSchedulerPriority, () => {
    // 执行 useEffect
    flushPassiveEffects();
    return null;
  });
  // BeforeMutation 阶段
  commitBeforeMutationEffects(root, finishedWork);
  // Mutation 阶段
  commitMutationEffects(root, finishedWork, lanes);
  // Layout 阶段
  commitLayoutEffects(finishedWork, root, lanes);
}先执行所有 useEffect 的 destroy 方法,然后才执行所有 useEffect 的 create 方法。并保持顺序是先子后父。
function flushPassiveEffectsImpl() {
  // ...
  // useEffect 的 destroy
  commitPassiveUnmountEffects(root.current);
  // useEffect 的 create
  commitPassiveMountEffects(root, root.current, lanes, transitions);
  // ...
}画个流程图:
create 表示传给 useEffect 的回调函数,destroy 为调用该回调函数返回的销毁函数。
总结一下。
commit 分成三个阶段:BeforeMuation、Muation 以及 Layout 阶段。
最后是 commit 阶段外的 useEffect,它被 Scheduler 异步调度执行,先执行完整棵树的 destroy,再执行完整棵树的 create。