commitMutationEffects
在 commitRootImpl()
中会调用 commitMutationEffects
,进入 mutation 阶段(即执行 DOM 操作)
commitMutationEffects(root, finishedWork, lanes)
源码地址 commitMutationEffects() | react-reconciler/src/ReactFiberCommitWork.old.js
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
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
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
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
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
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
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
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.'
)
}
}