在React的渲染流程中,commit阶段是一个关键的环节,它负责将虚拟DOM的变更最终反映到实际的DOM上。
一、commit过程
React将commit阶段分为多个子阶段:
before mutation: 处理副作用队列中BeforeMutationMaskflag标记的副作用mutation: 处理副作用队列中MutationMaskflag标记的副作用,以及DOM元素的增删改操作layout: 执行DOM操作后需要处理的副作用函数等逻辑
二、commit起点: commitRoot
commitRoot中对优先级进行了处理,并调用了commitRootImpl,以下代码只保留核心逻辑
function commitRootImpl(
root: FiberRoot,
recoverableErrors: null | Array<CapturedValue<mixed>>,
transitions: Array<Transition> | null,
didIncludeRenderPhaseUpdate: boolean,
renderPriorityLevel: EventPriority,
spawnedLane: Lane,
updatedLanes: Lanes,
suspendedRetryLanes: Lanes,
suspendedCommitReason: SuspendedCommitReason,
completedRenderStartTime: number,
completedRenderEndTime: number,
) {
const finishedWork = root.finishedWork;
const subtreeHasEffects =
(finishedWork.subtreeFlags &
(BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
NoFlags;
const rootHasEffect =
(finishedWork.flags &
(BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
NoFlags;
if (subtreeHasEffects || rootHasEffect) {
const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
root,
finishedWork,
);
commitMutationEffects(root, finishedWork, lanes);
resetAfterCommit(root.containerInfo);
root.current = finishedWork;
commitLayoutEffects(finishedWork, root, lanes);
} else {
root.current = finishedWork;
}
return null;
}
函数内部处理了很多东西: 清空passiveEffect、上下文检查、处理性能跟踪等,这些暂时不讨论。
commitRootImpl主要进行的操作是处理副作用队列, 渲染DOM节点;
可以看到函数内部首先拿到了在内存中构造的Fiber树finishedWork,对树上的副作用标记进行了判断,当根节点或者子孙节点存在副作用flag时才会进入处理过程;
if (subtreeHasEffects || rootHasEffect) {
//...
}
三、阶段 beforeMutation
beforeMutation的处理在函数commitBeforeMutationEffects中:
其中会经过几个子阶段:
- commitBeforeMutationEffects_begin
- commitBeforeMutationEffectsDeletion
- commitBeforeMutationEffects_complete
export function commitBeforeMutationEffects(
root: FiberRoot,
firstChild: Fiber,
): boolean {
focusedInstanceHandle = prepareForCommit(root.containerInfo);
nextEffect = firstChild;
commitBeforeMutationEffects_begin();
const shouldFire = shouldFireAfterActiveInstanceBlur;
shouldFireAfterActiveInstanceBlur = false;
focusedInstanceHandle = null;
return shouldFire;
}
commitBeforeMutationEffects_begin 子阶段
子阶段的开始根据不同条件进入下一子阶段
function commitBeforeMutationEffects_begin() {
while (nextEffect !== null) {
const fiber = nextEffect;
if (enableCreateEventHandleAPI) {
const deletions = fiber.deletions;
if (deletions !== null) {
for (let i = 0; i < deletions.length; i++) {
const deletion = deletions[i];
commitBeforeMutationEffectsDeletion(deletion);
}
}
}
const child = fiber.child;
if (
(fiber.subtreeFlags & BeforeMutationMask) !== NoFlags &&
child !== null
) {
child.return = fiber;
nextEffect = child;
} else {
commitBeforeMutationEffects_complete();
}
}
}
当开启了enableCreateEventHandleAPI,并且fiber.deletions存在时,处理需要在commit过程中删除
的节点进入commitBeforeMutationEffectsDeletion 子阶段
当fiber.subtreeFlags存在并且属于需要在该阶段进行处理的副作用类型,则指向下一个子节点,继续处理
当子节点全部处理完成之后会进入commitBeforeMutationEffects_complete
commitBeforeMutationEffects_complete 子阶段
function commitBeforeMutationEffects_complete() {
while (nextEffect !== null) {
const fiber = nextEffect;
commitBeforeMutationEffectsOnFiber(fiber);
const sibling = fiber.sibling;
if (sibling !== null) {
sibling.return = fiber.return;
nextEffect = sibling;
return;
}
nextEffect = fiber.return;
}
}
可以看到,主要的处理逻辑在commitBeforeMutationEffectsOnFiber中进行,处理完之后会寻找当前节点的兄弟节点并进行, 若兄弟节点处理完成,则会将指针往上指向父节点,在此进入commitBeforeMutationEffects_complete时处理父节点的兄弟节点,直至整个树处理完成;
commitBeforeMutationEffectsOnFiber
function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
const current = finishedWork.alternate;
const flags = finishedWork.flags;
switch (finishedWork.tag) {
case FunctionComponent: {
if (enableUseEffectEventHook) {
if ((flags & Update) !== NoFlags) {
const updateQueue: FunctionComponentUpdateQueue | null =
(finishedWork.updateQueue: any);
const eventPayloads =
updateQueue !== null ? updateQueue.events : null;
if (eventPayloads !== null) {
for (let ii = 0; ii < eventPayloads.length; ii++) {
const {ref, nextImpl} = eventPayloads[ii];
ref.impl = nextImpl;
}
}
}
}
break;
}
case ClassComponent: {
if ((flags & Snapshot) !== NoFlags) {
if (current !== null) {
commitClassSnapshot(finishedWork, current);
}
}
break;
}
case HostRoot: {
if ((flags & Snapshot) !== NoFlags) {
if (supportsMutation) {
const root = finishedWork.stateNode;
clearContainer(root.containerInfo);
}
}
break;
}
default: {
if ((flags & Snapshot) !== NoFlags) {
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.',
);
}
}
}
}
可以看到函数内部做了如下处理:
- 处理了针对函数式组件的
enableUseEffectEventHook钩子 - 类组件中调用了
commitClassSnapshot处理类组件快照 - 清空
HostRoot挂载的内容,方便后续处理
四、mutation阶段
mutation阶段主要逻辑在函数commitMutationEffectsOnFiber中,函数中代码比较多,可简略为如下结构:
function commitMutationEffectsOnFiber(
finishedWork: Fiber,
root: FiberRoot,
lanes: Lanes,
) {
const prevEffectStart = pushComponentEffectStart();
const current = finishedWork.alternate;
const flags = finishedWork.flags;
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
if (flags & Update) {
commitHookEffectListUnmount(
HookInsertion | HookHasEffect,
finishedWork,
finishedWork.return,
);
commitHookEffectListMount(HookInsertion | HookHasEffect, finishedWork);
commitHookLayoutUnmountEffects(
finishedWork,
finishedWork.return,
HookLayout | HookHasEffect,
);
}
break;
}
...
}
popComponentEffectStart(prevEffectStart);
}
可以看到内部针对不同类型的节点做了区分处理,不同组件的处理流程不一样,在需要处理的情况下有这几个过程:
删除
删除的逻辑主要是在recursivelyTraverseMutationEffects函数内部:
function recursivelyTraverseMutationEffects(
root: FiberRoot,
parentFiber: Fiber,
lanes: Lanes,
) {
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);
}
}
}
const prevDebugFiber = getCurrentDebugFiberInDEV();
if (parentFiber.subtreeFlags & MutationMask) {
let child = parentFiber.child;
while (child !== null) {
setCurrentDebugFiberInDEV(child);
commitMutationEffectsOnFiber(child, root, lanes);
child = child.sibling;
}
}
setCurrentDebugFiberInDEV(prevDebugFiber);
}
主要还是根据deletions标识去处理是否需要删除,需要删除的情况下commitDeletionEffects被调用,整个过程还是遵循深度优先遍历的原则进行。
commitDeletionEffects后续根据不同的节点类型做了大量的处理,暂时不细看,大体如下:
- 调用
safelyDetachRef函数清除节点的ref属性(HostComponent、ClassComponent等类型); - 调用
removeChildFromContainer和removeChild删除对应的子DOM节点(HostText类型); - 调用
clearSuspenseBoundary处理Suspense Boundary(DehydratedFragment类型); - 调用生命周期钩子
componentWillUnmount(ClassComponent类型)
插入 移动 水合操作
主要在函数commitReconciliationEffects中执行
function commitReconciliationEffects(finishedWork: Fiber) {
const flags = finishedWork.flags;
if (flags & Placement) {
try {
commitPlacement(finishedWork);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
finishedWork.flags &= ~Placement;
}
if (flags & Hydrating) {
finishedWork.flags &= ~Hydrating;
}
}
其中Placement标识Fiber节点需要插入或者移动处理,Hydrating代表需要进行水合相关操作
来看一下commitPlacement中发生了什么:
function commitPlacement(finishedWork: Fiber): void {
if (!supportsMutation) {
return;
}
const parentFiber = getHostParentFiber(finishedWork);
switch (parentFiber.tag) {
case HostComponent: {
const parent: Instance = parentFiber.stateNode;
if (parentFiber.flags & ContentReset) {
resetTextContent(parent);
parentFiber.flags &= ~ContentReset;
}
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;
}
default:
throw new Error(
'Invalid host parent fiber. This error is likely caused by a bug ' +
'in React. Please file an issue.',
);
}
}
不同情况下逻辑类似,只看HostComponent的情况
首先获取到可用的父节点,便于操作:
getHostParentFiber会找到待处理节点的可用祖先节点(类型为:HostComponent、HostRoot、HostPortal)getHostSibling会找到待处理节点的第一个兄弟节点的DOM元素insertOrAppendPlacementNode中会调用对应的方法处理节点的添加appendChild插入insertBefore操作,此过程会递归进行从而处理待处理节点的子节点
属性更新
当节点上存在Update flag时需要处理更新,以HostComponent属性的节点为例
if (flags & Update) {
const instance: Instance = finishedWork.stateNode;
if (instance != null)
const newProps = finishedWork.memoizedProps;
const oldProps =
current !== null ? current.memoizedProps : newProps;
const type = finishedWork.type;
const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);
finishedWork.updateQueue = null;
if (updatePayload !== null) {
try {
commitUpdate(
instance,
updatePayload,
type,
oldProps,
newProps,
finishedWork,
);
} catch (error) {
captureCommitPhaseError(
finishedWork,
finishedWork.return,
error,
);
}
}
}
}
可以看到属性更新主要进入commitUpdate处理
export function commitUpdate(
domElement: Instance,
updatePayload: Array<mixed>,
type: string,
oldProps: Props,
newProps: Props,
internalInstanceHandle: Object,
): void {
updateProperties(domElement, updatePayload, type, oldProps, newProps);
// 标记一下变化的参数有哪些
updateFiberProps(domElement, newProps);
}
经过updateProperties逻辑最终会走到updateDOMProperties函数中:
function updateDOMProperties(
domElement: Element,
updatePayload: Array<any>,
wasCustomComponentTag: boolean,
isCustomComponentTag: boolean,
): void {
for (let i = 0; i < updatePayload.length; i += 2) {
const propKey = updatePayload[i];
const propValue = updatePayload[i + 1];
if (propKey === STYLE) {
setValueForStyles(domElement, propValue);
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
setInnerHTML(domElement, propValue);
} else if (propKey === CHILDREN) {
setTextContent(domElement, propValue);
} else {
setValueForProperty(domElement, propKey, propValue, isCustomComponentTag);
}
}
}
可以看到针对不同类型的属性做了处理:
STYLE:style属性DANGEROUSLY_SET_INNER_HTML: innerHTMLCHILDREN:文本子节点其它function 、attribute等
FiberTree切换
在进入layout阶段之前还需要进行一步操作: 将处理好的最新的Fiber树切换为currentFiber树
root.current = finishedWork;
五、layout 阶段
类似beforeMutation, layout也分为多个子阶段:
commitLayoutEffects_begincommitLayoutMountEffects_complete
commitLayoutEffects_begin
commitLayoutEffects_begin主要做的工作是遍历节点,然后进入commitLayoutMountEffects_complete进行后续处理
commitLayoutMountEffects_complete
function commitLayoutMountEffects_complete(
subtreeRoot: Fiber,
root: FiberRoot,
committedLanes: Lanes,
) {
while (nextEffect !== null) {
const fiber = nextEffect;
if ((fiber.flags & LayoutMask) !== NoFlags) {
const current = fiber.alternate;
setCurrentDebugFiberInDEV(fiber);
try {
commitLayoutEffectOnFiber(root, current, fiber, committedLanes);
} catch (error) {
captureCommitPhaseError(fiber, fiber.return, error);
}
resetCurrentDebugFiberInDEV();
}
if (fiber === subtreeRoot) {
nextEffect = null;
return;
}
const sibling = fiber.sibling;
if (sibling !== null) {
sibling.return = fiber.return;
nextEffect = sibling;
return;
}
nextEffect = fiber.return;
}
}
主要的逻辑在commitLayoutEffectOnFiber中进行
function commitLayoutEffectOnFiber(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
committedLanes: Lanes,
): void {
if ((finishedWork.flags & LayoutMask) !== NoFlags) {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
// ...
break;
}
case ClassComponent: {
// ...
break;
}
case HostRoot: {
break;
}
case HostComponent: {
// ...
break;
}
case HostText: {
// We have no life-cycles associated with text.
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.',
);
}
}
}
代码量很多,主要也是针对不同类型的节点分类处理:
- 调用
commitUpdateQueue执行更新后的节点的副作用队列的callback - 调用
componentDidMount、componentDidUpdate钩子 (ClassComponent类型) - 调用
commitMount处理DOM元素上的autoFocus属性 (HostComponent类型) - 触发
useLayoutEffect钩子(FunctionComponent类型)
总结
本文详细介绍了React的commit过程,包括其子阶段和核心逻辑。commit过程是React渲染流程的一部分,负责将虚拟DOM的变化应用到实际的DOM上。文章分为以下几个部分:
- Commit过程:React将commit阶段分为三个子阶段:
before mutation、mutation和layout,分别处理不同类型的副作用和DOM操作。 - Commit起点:
commitRoot是commit过程的起点,它处理优先级并调用commitRootImpl函数,后者主要负责处理副作用队列和渲染DOM节点。 - BeforeMutation阶段:
beforeMutation阶段处理BeforeMutationMask标记的副作用,包括删除操作和对DOM元素的准备工作。 - Mutation阶段:
mutation阶段处理MutationMask标记的副作用,包括DOM元素的增删改操作。 - Layout阶段:
layout阶段处理DOM操作后的副作用函数等逻辑,包括触发生命周期钩子和useLayoutEffect钩子。