跳到主要内容

认识

mutation 执行DOM操作

工作


类似before mutation阶段,mutation阶段也是遍历effectList,执行函数。这里执行的是commitMutationEffects。整个过程的工作如下:

原理


/react/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
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节点执行如下三个操作:

  1. 根据ContentReset effectTag重置文字节点

  2. 更新ref

  3. 根据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

/react/packages/react-reconciler/src/ReactFiberWorkLoop.new.js
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 工作分为三步

  1. 获取父级DOM节点。其中finishedWork为传入的Fiber节点

  2. 获取Fiber节点的DOM兄弟节点

  3. 根据DOM兄弟节点是否存在决定调用parentNode.insertBeforeparentNode.appendChild执行DOM插入操作。

注意点

  1. 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 工作如下:

  1. fiber.tagFunctionComponent时: 会调用commitHookEffectListUnmount。该方法会遍历effectList,执行所有useLayoutEffect hook的销毁函数。

  2. fiber.tagHostComponent时: 会调用commitUpdate。最终会在updateDOMProperties中将render阶段 completeWork中为Fiber节点赋值的updateQueue对应的内容渲染在页面上。

commitHookEffectListUnmount

/react/packages/react-reconciler/src/ReactFiberCommitWork.new.js
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

/react/packages/react-dom/src/client/ReactDOMHostConfig.js
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工作如下:

  1. 递归调用Fiber节点及其子孙Fiber节点中fiber.tagClassComponentcomponentWillUnmount生命周期钩子,从页面移除Fiber节点对应DOM节点

  2. 解绑ref

  3. 调度useEffect的销毁函数