Skip to content

鼓励作者:欢迎 star 或打赏犒劳

commitMutationEffects

commitRootImpl() 中会调用 commitMutationEffects,进入 mutation 阶段(即执行 DOM 操作)

ts
commitMutationEffects(root, finishedWork, lanes)

源码地址 commitMutationEffects() | react-reconciler/src/ReactFiberCommitWork.old.js

ts
export function commitMutationEffects(root: FiberRoot, finishedWork: Fiber, committedLanes: Lanes) {
  inProgressLanes = committedLanes
  inProgressRoot = root

  commitMutationEffectsOnFiber(finishedWork, root, committedLanes)

  inProgressLanes = null
  inProgressRoot = null
}

commitMutationEffectsOnFiber

commitMutationEffectsOnFiber 函数中会处理进行如下操作:

  • 调用 recursivelyTraverseMutationEffects 函数递归遍历子 Fiber 树
  • 调用 commitReconciliationEffects 函数
  • 处理 Update 副作用(更新)
  • 处理 Ref 副作用(卸载 Ref)
  • 处理 ContentReset 副作用(重置文本内容)

源码地址 commitMutationEffectsOnFiber() | react-reconciler/src/ReactFiberCommitWork.old.js

ts
function commitMutationEffectsOnFiber(
  finishedWork: Fiber,
  root: FiberRoot,
  lanes: Lanes,
) {
  const current = finishedWork.alternate;
  const flags = finishedWork.flags;

  // The effect flag should be checked *after* we refine the type of fiber,
  // because the fiber tag is more specific. An exception is any flag related
  // to reconcilation, because those can be set on all fiber types.

  // 根据 tag 进入不同的处理函数
  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case MemoComponent:
    case SimpleMemoComponent: {
      recursivelyTraverseMutationEffects(root, finishedWork, lanes);
      commitReconciliationEffects(finishedWork);

      // 处理 Update 副作用
      if (flags & Update) {
        try {
          // 执行 useEffect 卸载函数
          commitHookEffectListUnmount(
            HookInsertion | HookHasEffect,
            finishedWork,
            finishedWork.return,
          );
          // 执行 useEffect 挂载函数
          commitHookEffectListMount(
            HookInsertion | HookHasEffect,
            finishedWork,
          );
        } catch (error) {
          captureCommitPhaseError(finishedWork, finishedWork.return, error);
        }
        // Layout effects are destroyed during the mutation phase so that all
        // destroy functions for all fibers are called before any create functions.
        // This prevents sibling component effects from interfering with each other,
        // e.g. a destroy function in one component should never override a ref set
        // by a create function in another component during the same commit.
        // 执行依赖更新时的 useLayoutEffect 卸载函数
        if (
          enableProfilerTimer &&
          enableProfilerCommitHooks &&
          finishedWork.mode & ProfileMode
        ) {
          try {
            startLayoutEffectTimer();
            commitHookEffectListUnmount(
              HookLayout | HookHasEffect,
              finishedWork,
              finishedWork.return,
            );
          } catch (error) {
            captureCommitPhaseError(finishedWork, finishedWork.return, error);
          }
          recordLayoutEffectDuration(finishedWork);
        } else {
          try {
            commitHookEffectListUnmount(
              HookLayout | HookHasEffect,
              finishedWork,
              finishedWork.return,
            );
          } catch (error) {
            captureCommitPhaseError(finishedWork, finishedWork.return, error);
          }
        }
      }
      return;
    }
    case ClassComponent: {
      recursivelyTraverseMutationEffects(root, finishedWork, lanes);
      commitReconciliationEffects(finishedWork);

      // 存在 Ref 副作用时,卸载 Ref
      if (flags & Ref) {
        if (current !== null) {
          safelyDetachRef(current, current.return);
        }
      }
      return;
    }
    case HostComponent: {
      recursivelyTraverseMutationEffects(root, finishedWork, lanes);
      commitReconciliationEffects(finishedWork);

      // 处理 Ref 副作用(卸载 Ref)
      if (flags & Ref) {
        if (current !== null) {
          safelyDetachRef(current, current.return);
        }
      }
      if (supportsMutation) {
        // TODO: ContentReset gets cleared by the children during the commit
        // phase. This is a refactor hazard because it means we must read
        // flags the flags after `commitReconciliationEffects` has already run;
        // the order matters. We should refactor so that ContentReset does not
        // rely on mutating the flag during commit. Like by setting a flag
        // during the render phase instead.

        // 处理 ContentReset 副作用(重置文本内容)
        if (finishedWork.flags & ContentReset) {
          const instance: Instance = finishedWork.stateNode;
          try {
            // 重置文本内容
            resetTextContent(instance);
          } catch (error) {
            captureCommitPhaseError(finishedWork, finishedWork.return, error);
          }
        }

        // 处理 Update 副作用(更新 DOM 节点)
        if (flags & Update) {
          const instance: Instance = finishedWork.stateNode;
          if (instance != null) {
            // Commit the work prepared earlier.
            const newProps = finishedWork.memoizedProps;
            // For hydration we reuse the update path but we treat the oldProps
            // as the newProps. The updatePayload will contain the real change in
            // this case.
            const oldProps =
              current !== null ? current.memoizedProps : newProps;
            const type = finishedWork.type;
            // TODO: Type the updateQueue to be specific to host components.
            const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);
            finishedWork.updateQueue = null;
            if (updatePayload !== null) {
              try {
                // 更新 DOM 节点
                commitUpdate(
                  instance,
                  updatePayload,
                  type,
                  oldProps,
                  newProps,
                  finishedWork,
                );
              } catch (error) {
                captureCommitPhaseError(
                  finishedWork,
                  finishedWork.return,
                  error,
                );
              }
            }
          }
        }
      }
      return;
    }
    case HostText: {
      recursivelyTraverseMutationEffects(root, finishedWork, lanes);
      commitReconciliationEffects(finishedWork);

      // 处理 Update 副作用(更新文本节点)
      if (flags & Update) {
        if (supportsMutation) {
          if (finishedWork.stateNode === null) {
            throw new Error(
              'This should have a text node initialized. This error is likely ' +
                'caused by a bug in React. Please file an issue.',
            );
          }

          const textInstance: TextInstance = finishedWork.stateNode;
          const newText: string = finishedWork.memoizedProps;
          // For hydration we reuse the update path but we treat the oldProps
          // as the newProps. The updatePayload will contain the real change in
          // this case.
          const oldText: string =
            current !== null ? current.memoizedProps : newText;

          try {
            // 更新文本节点
            commitTextUpdate(textInstance, oldText, newText);
          } catch (error) {
            captureCommitPhaseError(finishedWork, finishedWork.return, error);
          }
        }
      }
      return;
    }
    case HostRoot:
    case HostPortal:
    case SuspenseComponent:
    case OffscreenComponent:
    case SuspenseListComponent:
    case ScopeComponent:
    default: {
      recursivelyTraverseMutationEffects(root, finishedWork, lanes);
      commitReconciliationEffects(finishedWork);
      return;
    }
  }
}

recursivelyTraverseMutationEffects

recursivelyTraverseMutationEffects 函数递归遍历子 Fiber 树,并做如下操作:

  • 调用 commitDeletionEffects 函数处理 Deletion 副作用(删除)
  • 调用 commitMutationEffectsOnFiber 函数处理 Mutation 副作用(插入、更新、删除、Ref 等)

源码地址 recursivelyTraverseMutationEffects() | react-reconciler/src/ReactFiberCommitWork.old.js

ts
function recursivelyTraverseMutationEffects(root: FiberRoot, parentFiber: Fiber, lanes: Lanes) {
  // Deletions effects can be scheduled on any fiber type. They need to happen
  // before the children effects hae fired.
  // 处理 Deletion 副作用(即 ChildDeletion)
  const deletions = parentFiber.deletions
  if (deletions !== null) {
    for (let i = 0; i < deletions.length; i++) {
      const childToDelete = deletions[i]
      try {
        commitDeletionEffects(root, parentFiber, childToDelete)
      } catch (error) {
        captureCommitPhaseError(childToDelete, parentFiber, error)
      }
    }
  }

  // 处理 Mutation 副作用(即 Placement | Update | ChildDeletion | ContentReset | Ref | Hydrating | Visibility)
  if (parentFiber.subtreeFlags & MutationMask) {
    let child = parentFiber.child
    while (child !== null) {
      commitMutationEffectsOnFiber(child, root, lanes)
      child = child.sibling
    }
  }
}

commitDeletionEffects

源码地址 commitDeletionEffects() | react-reconciler/src/ReactFiberCommitWork.old.js

ts
function commitDeletionEffects(root: FiberRoot, returnFiber: Fiber, deletedFiber: Fiber) {
  if (supportsMutation) {
    // We only have the top Fiber that was deleted but we need to recurse down its
    // children to find all the terminal nodes.

    // Recursively delete all host nodes from the parent, detach refs, clean
    // up mounted layout effects, and call componentWillUnmount.

    // We only need to remove the topmost host child in each branch. But then we
    // still need to keep traversing to unmount effects, refs, and cWU. TODO: We
    // could split this into two separate traversals functions, where the second
    // one doesn't include any removeChild logic. This is maybe the same
    // function as "disappearLayoutEffects" (or whatever that turns into after
    // the layout phase is refactored to use recursion).

    // Before starting, find the nearest host parent on the stack so we know
    // which instance/container to remove the children from.
    // TODO: Instead of searching up the fiber return path on every deletion, we
    // can track the nearest host component on the JS stack as we traverse the
    // tree during the commit phase. This would make insertions faster, too.
    let parent = returnFiber
    // 标记父节点是否为容器
    findParent: while (parent !== null) {
      switch (parent.tag) {
        case HostComponent: {
          hostParent = parent.stateNode
          hostParentIsContainer = false
          break findParent
        }
        case HostRoot: {
          hostParent = parent.stateNode.containerInfo
          hostParentIsContainer = true
          break findParent
        }
        case HostPortal: {
          hostParent = parent.stateNode.containerInfo
          hostParentIsContainer = true
          break findParent
        }
      }
      parent = parent.return
    }
    if (hostParent === null) {
      throw new Error(
        'Expected to find a host parent. This error is likely caused by ' +
          'a bug in React. Please file an issue.'
      )
    }
    commitDeletionEffectsOnFiber(root, returnFiber, deletedFiber)
    hostParent = null
    hostParentIsContainer = false
  } else {
    // Detach refs and call componentWillUnmount() on the whole subtree.
    commitDeletionEffectsOnFiber(root, returnFiber, deletedFiber)
  }

  detachFiberMutation(deletedFiber)
}

commitDeletionEffectsOnFiber

源码地址 commitDeletionEffectsOnFiber() | react-reconciler/src/ReactFiberCommitWork.old.js

ts
function commitDeletionEffectsOnFiber(
  finishedRoot: FiberRoot,
  nearestMountedAncestor: Fiber,
  deletedFiber: Fiber
) {
  // The cases in this outer switch modify the stack before they traverse
  // into their subtree. There are simpler cases in the inner switch
  // that don't modify the stack.
  switch (deletedFiber.tag) {
    case HostComponent: {
      if (!offscreenSubtreeWasHidden) {
        safelyDetachRef(deletedFiber, nearestMountedAncestor)
      }
      // Intentional fallthrough to next branch
    }
    // eslint-disable-next-line-no-fallthrough
    case HostText: {
      // We only need to remove the nearest host child. Set the host parent
      // to `null` on the stack to indicate that nested children don't
      // need to be removed.
      if (supportsMutation) {
        const prevHostParent = hostParent
        const prevHostParentIsContainer = hostParentIsContainer
        hostParent = null
        // 继续递归遍历执行 recursivelyTraverseDeletionEffects() 函数
        recursivelyTraverseDeletionEffects(finishedRoot, nearestMountedAncestor, deletedFiber)
        hostParent = prevHostParent
        hostParentIsContainer = prevHostParentIsContainer

        if (hostParent !== null) {
          // Now that all the child effects have unmounted, we can remove the
          // node from the tree.
          // 判断是否父节点是否为容器
          if (hostParentIsContainer) {
            // 从容器中删除子节点
            removeChildFromContainer(hostParent, deletedFiber.stateNode)
          } else {
            // 直接删除子节点
            removeChild(hostParent, deletedFiber.stateNode)
          }
        }
      } else {
        // 继续递归遍历执行 commitDeletionEffectsOnFiber() 函数
        recursivelyTraverseDeletionEffects(finishedRoot, nearestMountedAncestor, deletedFiber)
      }
      return
    }

    // ... 省略以下 case 的处理逻辑
    case DehydratedFragment:
    case HostPortal:

    // 函数组件
    case FunctionComponent:
    case ForwardRef:
    case MemoComponent:
    case SimpleMemoComponent: {
      if (!offscreenSubtreeWasHidden) {
        const updateQueue: FunctionComponentUpdateQueue | null = deletedFiber.updateQueue
        if (updateQueue !== null) {
          const lastEffect = updateQueue.lastEffect
          if (lastEffect !== null) {
            const firstEffect = lastEffect.next

            let effect = firstEffect
            do {
              const { destroy, tag } = effect
              // 判断是否存在 destroy 函数
              if (destroy !== undefined) {
                if ((tag & HookInsertion) !== NoHookEffect) {
                  // 执行组件的 useEffect 卸载函数
                  safelyCallDestroy(deletedFiber, nearestMountedAncestor, destroy)
                } else if ((tag & HookLayout) !== NoHookEffect) {
                  if (enableSchedulingProfiler) {
                    markComponentLayoutEffectUnmountStarted(deletedFiber)
                  }

                  // 执行组件的 useLayoutEffect 卸载函数
                  if (
                    enableProfilerTimer &&
                    enableProfilerCommitHooks &&
                    deletedFiber.mode & ProfileMode
                  ) {
                    startLayoutEffectTimer()
                    safelyCallDestroy(deletedFiber, nearestMountedAncestor, destroy)
                    recordLayoutEffectDuration(deletedFiber)
                  } else {
                    safelyCallDestroy(deletedFiber, nearestMountedAncestor, destroy)
                  }

                  if (enableSchedulingProfiler) {
                    markComponentLayoutEffectUnmountStopped()
                  }
                }
              }
              effect = effect.next
            } while (effect !== firstEffect)
          }
        }
      }

      // 继续递归遍历执行 commitDeletionEffectsOnFiber() 函数
      recursivelyTraverseDeletionEffects(finishedRoot, nearestMountedAncestor, deletedFiber)
      return
    }
    // 类组件
    case ClassComponent: {
      if (!offscreenSubtreeWasHidden) {
        // 卸载 Ref
        safelyDetachRef(deletedFiber, nearestMountedAncestor)
        const instance = deletedFiber.stateNode
        // 执行类组件的 componentWillUnmount 生命周期函数
        if (typeof instance.componentWillUnmount === 'function') {
          safelyCallComponentWillUnmount(deletedFiber, nearestMountedAncestor, instance)
        }
      }
      recursivelyTraverseDeletionEffects(finishedRoot, nearestMountedAncestor, deletedFiber)
      return
    }

    // ... 省略以下 case 的处理逻辑
    case ScopeComponent:
    case OffscreenComponent:
    default: {
      recursivelyTraverseDeletionEffects(finishedRoot, nearestMountedAncestor, deletedFiber)
      return
    }
  }
}

recursivelyTraverseDeletionEffects

对子 Fiber 树进行遍历,并调用 commitDeletionEffectsOnFiber 函数处理 Deletion 副作用(删除)

源码地址 recursivelyTraverseDeletionEffects() | react-reconciler/src/ReactFiberCommitWork.old.js

ts
function recursivelyTraverseDeletionEffects(finishedRoot, nearestMountedAncestor, parent) {
  let child = parent.child
  while (child !== null) {
    commitDeletionEffectsOnFiber(finishedRoot, nearestMountedAncestor, child)
    child = child.sibling
  }
}

commitReconciliationEffects

commitReconciliationEffects 函数用于处理 Placement 副作用(插入)

源码地址 commitReconciliationEffects() | react-reconciler/src/ReactFiberCommitWork.old.js

ts
function commitReconciliationEffects(finishedWork: Fiber) {
  // Placement effects (insertions, reorders) can be scheduled on any fiber
  // type. They needs to happen after the children effects have fired, but
  // before the effects on this fiber have fired.
  const flags = finishedWork.flags
  // 判断是否存在 Placement 副作用
  if (flags & Placement) {
    try {
      commitPlacement(finishedWork)
    } catch (error) {
      captureCommitPhaseError(finishedWork, finishedWork.return, error)
    }
    // Clear the "placement" from effect tag so that we know that this is
    // inserted, before any life-cycles like componentDidMount gets called.
    // TODO: findDOMNode doesn't rely on this any more but isMounted does
    // and isMounted is deprecated anyway so we should be able to kill this.
    // 去除 flags 中 Placement 的标记
    finishedWork.flags &= ~Placement
  }
  if (flags & Hydrating) {
    finishedWork.flags &= ~Hydrating
  }
}

commitPlacement

源码地址 commitPlacement() | react-reconciler/src/ReactFiberCommitWork.old.js

ts
function commitPlacement(finishedWork: Fiber): void {
  if (!supportsMutation) {
    return
  }

  // Recursively insert all host nodes into the parent.
  // 获取 tag 为 HostComponent、HostRoot、HostPortal 的父节点
  const parentFiber = getHostParentFiber(finishedWork)

  // Note: these two variables *must* always be updated together.
  // 判断父 Fiber 节点的 tag
  switch (parentFiber.tag) {
    // 普通 DOM 标签
    case HostComponent: {
      // 获取 DOM 节点
      const parent: Instance = parentFiber.stateNode
      // 当 flags 中存在 ContentReset 副作用时,重置父节点的文本内容
      if (parentFiber.flags & ContentReset) {
        // Reset the text content of the parent before doing any insertions
        resetTextContent(parent)
        // Clear ContentReset from the effect tag
        // 去除 flags 中 ContentReset 的标记
        parentFiber.flags &= ~ContentReset
      }

      // 获取当前 Fiber 节点的 DOM 兄弟节点
      const before = getHostSibling(finishedWork)
      // We only have the top Fiber that was inserted but we need to recurse down its
      // children to find all the terminal nodes.
      // 递归插入所有的子节点
      insertOrAppendPlacementNode(finishedWork, before, parent)
      break
    }
    // React 根节点
    case HostRoot:
    // React Portal 节点
    case HostPortal: {
      // 获取 DOM 节点
      const parent: Container = parentFiber.stateNode.containerInfo
      // 获取当前 Fiber 节点的 DOM 兄弟节点
      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.'
      )
  }
}

如有转载或 CV 的请标注本站原文地址