认识
mutation
执行DOM操作
工作
类似before mutation
阶段,mutation
阶段也是遍历effectList
,执行函数。这里执行的是commitMutationEffects
。整个过程的工作如下:
原理
function commitMutationEffects(
firstChild: Fiber,
root: FiberRoot,
renderPriorityLevel: ReactPriorityLevel,
) {
let fiber = firstChild;
while (fiber !== null) {
const deletions = fiber.deletions;
if (deletions !== null) {
commitMutationEffectsDeletions(
deletions,
fiber,
root,
renderPriorityLevel,
);
}
if (fiber.child !== null) {
const mutationFlags = fiber.subtreeFlags & MutationMask;
if (mutationFlags !== NoFlags) {
commitMutationEffects(fiber.child, root, renderPriorityLevel);
}
}
if (__DEV__) {
setCurrentDebugFiberInDEV(fiber);
invokeGuardedCallback(
null,
commitMutationEffectsImpl,
null,
fiber,
root,
renderPriorityLevel,
);
if (hasCaughtError()) {
const error = clearCaughtError();
captureCommitPhaseError(fiber, fiber.return, error);
}
resetCurrentDebugFiberInDEV();
} else {
try {
commitMutationEffectsImpl(fiber, root, renderPriorityLevel);
} catch (error) {
captureCommitPhaseError(fiber, fiber.return, error);
}
}
fiber = fiber.sibling;
}
}
commitMutationEffects
会遍历effectList
,对每个Fiber
节点执行如下三个操作:
-
根据
ContentReset effectTag
重置文字节点 -
更新
ref
-
根据
effectTag
分别处理,其中effectTag
包括(Placement | Update | Deletion | Hydrating)
commitMutationEffectsDeletions
function commitMutationEffectsDeletions(
deletions: Array<Fiber>,
nearestMountedAncestor: Fiber,
root: FiberRoot,
renderPriorityLevel,
) {
for (let i = 0; i < deletions.length; i++) {
const childToDelete = deletions[i];
if (__DEV__) {
invokeGuardedCallback(
null,
commitDeletion,
null,
root,
childToDelete,
nearestMountedAncestor,
renderPriorityLevel,
);
if (hasCaughtError()) {
const error = clearCaughtError();
captureCommitPhaseError(childToDelete, nearestMountedAncestor, error);
}
} else {
try {
commitDeletion(
root,
childToDelete,
nearestMountedAncestor,
renderPriorityLevel,
);
} catch (error) {
captureCommitPhaseError(childToDelete, nearestMountedAncestor, error);
}
}
}
}
commitMutationEffectsImpl
function commitMutationEffectsImpl(
fiber: Fiber,
root: FiberRoot,
renderPriorityLevel,
) {
const flags = fiber.flags;
if (flags & ContentReset) {
commitResetTextContent(fiber);
}
if (flags & Ref) {
const current = fiber.alternate;
if (current !== null) {
commitDetachRef(current);
}
if (enableScopeAPI) {
// TODO: This is a temporary solution that allowed us to transition away from React Flare on www.
if (fiber.tag === ScopeComponent) {
commitAttachRef(fiber);
}
}
}
// The following switch statement is only concerned about placement,
// updates, and deletions. To avoid needing to add a case for every possible
// bitmap value, we remove the secondary effects from the effect tag and
// switch on that value.
const primaryFlags = flags & (Placement | Update | Hydrating);
switch (primaryFlags) {
case Placement: {
commitPlacement(fiber);
// 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.
fiber.flags &= ~Placement;
break;
}
case PlacementAndUpdate: {
// Placement
commitPlacement(fiber);
// Clear the "placement" from effect tag so that we know that this is
// inserted, before any life-cycles like componentDidMount gets called.
fiber.flags &= ~Placement;
// Update
const current = fiber.alternate;
commitWork(current, fiber);
break;
}
case Hydrating: {
fiber.flags &= ~Hydrating;
break;
}
case HydratingAndUpdate: {
fiber.flags &= ~Hydrating;
// Update
const current = fiber.alternate;
commitWork(current, fiber);
break;
}
case Update: {
const current = fiber.alternate;
commitWork(current, fiber);
break;
}
}
}
commitMutationEffectsImpl 工作如下:
-
根据
effectTag
分别处理,其中effectTag
包括(Placement
|Update
|Deletion
|Hydrating
)-
Placement: 当
Fiber
节点含有Placement effectTag
,意味着该Fiber
节点对应的DOM
节点需要插入到页面中,调用的方法为commitPlacement
-
Update: 当
Fiber
节点含有Update effectTag
,意味着该Fiber
节点需要更新。调用的方法为commitWork
-
Deletion: 当
Fiber
节点含有Deletion effectTag
时: 意味着该Fiber
节点对应的DOM
节点需要从页面中删除。调用的方法为commitDeletion
-
commitPlacement
function commitPlacement(finishedWork: Fiber): void {
if (!supportsMutation) {
return;
}
// Recursively insert all host nodes into the parent.
const parentFiber = getHostParentFiber(finishedWork);
// Note: these two variables *must* always be updated together.
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;
case FundamentalComponent:
if (enableFundamentalAPI) {
parent = parentStateNode.instance;
isContainer = false;
}
// eslint-disable-next-line-no-fallthrough
default:
invariant(
false,
'Invalid host parent fiber. This error is likely caused by a bug ' +
'in React. Please file an issue.',
);
}
if (parentFiber.flags & ContentReset) {
// Reset the text content of the parent before doing any insertions
resetTextContent(parent);
// Clear ContentReset from the effect tag
parentFiber.flags &= ~ContentReset;
}
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.
if (isContainer) {
insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
} else {
insertOrAppendPlacementNode(finishedWork, before, parent);
}
}
commitPlacement
工作分为三步
-
获取父级
DOM
节点。其中finishedWork
为传入的Fiber节点 -
获取
Fiber
节点的DOM
兄弟节点 -
根据
DOM
兄弟节点是否存在决定调用parentNode.insertBefore
或parentNode.appendChild
执行DOM
插入操作。
注意点
getHostSibling
(获取兄弟DOM节点)的执行很耗时,当在同一个父Fiber节点下依次执行多个插入操作,getHostSibling
算法的复杂度为指数级。这是由于Fiber
节点不只包括HostComponent
,所以Fiber
树和渲染的DOM
树节点并不是一一对应的。要从Fiber
节点找到DOM
节点很可能跨层级遍历
commitWork
function commitWork(current: Fiber | null, finishedWork: Fiber): void {
if (!supportsMutation) {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent:
case Block: {
// 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.
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
finishedWork.mode & ProfileMode
) {
try {
startLayoutEffectTimer();
commitHookEffectListUnmount(
HookLayout | HookHasEffect,
finishedWork,
finishedWork.return,
);
} finally {
recordLayoutEffectDuration(finishedWork);
}
} else {
commitHookEffectListUnmount(
HookLayout | HookHasEffect,
finishedWork,
finishedWork.return,
);
}
return;
}
case Profiler: {
return;
}
case SuspenseComponent: {
commitSuspenseComponent(finishedWork);
attachSuspenseRetryListeners(finishedWork);
return;
}
case SuspenseListComponent: {
attachSuspenseRetryListeners(finishedWork);
return;
}
case HostRoot: {
if (supportsHydration) {
const root: FiberRoot = finishedWork.stateNode;
if (root.hydrate) {
// We've just hydrated. No need to hydrate again.
root.hydrate = false;
commitHydratedContainer(root.containerInfo);
}
}
break;
}
case OffscreenComponent:
case LegacyHiddenComponent: {
return;
}
}
commitContainer(finishedWork);
return;
}
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent:
case Block: {
// 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.
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
finishedWork.mode & ProfileMode
) {
try {
startLayoutEffectTimer();
commitHookEffectListUnmount(
HookLayout | HookHasEffect,
finishedWork,
finishedWork.return,
);
} finally {
recordLayoutEffectDuration(finishedWork);
}
} else {
commitHookEffectListUnmount(
HookLayout | HookHasEffect,
finishedWork,
finishedWork.return,
);
}
return;
}
case ClassComponent: {
return;
}
case HostComponent: {
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) {
commitUpdate(
instance,
updatePayload,
type,
oldProps,
newProps,
finishedWork,
);
}
}
return;
}
case HostText: {
invariant(
finishedWork.stateNode !== null,
'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;
commitTextUpdate(textInstance, oldText, newText);
return;
}
case HostRoot: {
if (supportsHydration) {
const root: FiberRoot = finishedWork.stateNode;
if (root.hydrate) {
// We've just hydrated. No need to hydrate again.
root.hydrate = false;
commitHydratedContainer(root.containerInfo);
}
}
return;
}
case Profiler: {
return;
}
case SuspenseComponent: {
commitSuspenseComponent(finishedWork);
attachSuspenseRetryListeners(finishedWork);
return;
}
case SuspenseListComponent: {
attachSuspenseRetryListeners(finishedWork);
return;
}
case IncompleteClassComponent: {
return;
}
case FundamentalComponent: {
if (enableFundamentalAPI) {
const fundamentalInstance = finishedWork.stateNode;
updateFundamentalComponent(fundamentalInstance);
return;
}
break;
}
case ScopeComponent: {
if (enableScopeAPI) {
const scopeInstance = finishedWork.stateNode;
prepareScopeUpdate(scopeInstance, finishedWork);
return;
}
break;
}
case OffscreenComponent:
case LegacyHiddenComponent: {
const newState: OffscreenState | null = finishedWork.memoizedState;
const isHidden = newState !== null;
hideOrUnhideAllChildren(finishedWork, isHidden);
return;
}
}
invariant(
false,
'This unit of work tag should not have side-effects. This error is ' +
'likely caused by a bug in React. Please file an issue.',
);
}
commitWork
工作如下:
-
当
fiber.tag
为FunctionComponent
时: 会调用commitHookEffectListUnmount
。该方法会遍历effectList
,执行所有useLayoutEffect hook
的销毁函数。 -
当
fiber.tag
为HostComponent
时: 会调用commitUpdate
。最终会在updateDOMProperties
中将render
阶段completeWork
中为Fiber
节点赋值的updateQueue
对应的内容渲染在页面上。
commitHookEffectListUnmount
function commitHookEffectListUnmount(
flags: HookFlags,
finishedWork: Fiber,
nearestMountedAncestor: Fiber | null,
) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & flags) === flags) {
// Unmount
const destroy = effect.destroy;
effect.destroy = undefined;
if (destroy !== undefined) {
safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
commitUpdate
export function commitUpdate(
domElement: Instance,
updatePayload: Array<mixed>,
type: string,
oldProps: Props,
newProps: Props,
internalInstanceHandle: Object,
): void {
// Update the props handle so that we know which props are the ones with
// with current event handlers.
updateFiberProps(domElement, newProps);
// Apply the diff to the DOM node.
updateProperties(domElement, updatePayload, type, oldProps, newProps);
}
commitUpdate
工作如下:
commitDeletion
function commitDeletion(
finishedRoot: FiberRoot,
current: Fiber,
nearestMountedAncestor: Fiber,
renderPriorityLevel: ReactPriorityLevel,
): void {
if (supportsMutation) {
// Recursively delete all host nodes from the parent.
// Detach refs and call componentWillUnmount() on the whole subtree.
unmountHostComponents(
finishedRoot,
current,
nearestMountedAncestor,
renderPriorityLevel,
);
} else {
// Detach refs and call componentWillUnmount() on the whole subtree.
commitNestedUnmounts(
finishedRoot,
current,
nearestMountedAncestor,
renderPriorityLevel,
);
}
const alternate = current.alternate;
detachFiberMutation(current);
if (alternate !== null) {
detachFiberMutation(alternate);
}
}
commitDeletion
工作如下:
-
递归调用
Fiber
节点及其子孙Fiber
节点中fiber.tag
为ClassComponent
的componentWillUnmount
生命周期钩子,从页面移除Fiber
节点对应DOM
节点 -
解绑
ref
-
调度
useEffect
的销毁函数