在本系列的第二,第三章节已经完整的讲述了render
阶段的scheduler
调度流程和reconciler
协调流程。
本节将深入理解react应用渲染流程的最后一个阶段commit
阶段。
commitRootImpl
接着第二章节结束的内容,继续查看commitRootImpl
方法:
js
// packages\react-reconciler\src\ReactFiberWorkLoop.new.js
function commitRootImpl() {
...
}
这里的commitRootImpl
方法就是commit
阶段的全部内容,但是commitRootImpl
方法里面内容非常多,这里我们将commitRootImpl
方法的内容按执行顺序分成两个部分:
- 准备工作:commit阶段的准备工作。
- 主要工作:commit阶段的主要工作。
其中主要工作又可以分成三个子阶段 ,它是commit
的核心逻辑,后面将会详细讲解。
准备工作
js
// packages\react-reconciler\src\ReactFiberWorkLoop.new.js
function commitRootImpl() {
do {
// 循环处理副作用Effects,直到处理完成
flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);
// 取出要提交的工作: 代表workInProgress HostRootFiber 即render阶段构建的workInProgress Tree的HostRootFiber
const finishedWork = root.finishedWork;
// 取出Lanes
const lanes = root.finishedLanes;
// 重置root状态
root.finishedWork = null;
root.finishedLanes = NoLanes;
// 重置回调任务与优先级
root.callbackNode = null;
root.callbackPriority = NoLane;
// 合并同一批次的lanes,通过位运算,产生新的lane
let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes);
const concurrentlyUpdatedLanes = getConcurrentlyUpdatedLanes();
remainingLanes = mergeLanes(remainingLanes, concurrentlyUpdatedLanes);
// 标记root完成
markRootFinished(root, remainingLanes);
// 如果存在挂起的副作用,使用调度器生成一个task任务来处理它们
if (
(finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
(finishedWork.flags & PassiveMask) !== NoFlags
) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
pendingPassiveEffectsRemainingLanes = remainingLanes;
// Passive表示FC组件中定义了useEffect且需要触发的回调函数
pendingPassiveTransitions = transitions;
// 调度一个宏任务,处理函数组件的effects【只是调度了一个异步任务,effect回调实际上还没有处理】
scheduleCallback(NormalSchedulerPriority, () => {
// 这个回调就是处理useEffect的
flushPassiveEffects();
});
}
}
...
}
首先一进入commitRootImpl
方法,就是一个do while
循环,这个循环体调用了一个flushPassiveEffects
方法,Passive
标记对应的是useEffect
的副作用操作,所以这个方法的作用就是冲刷函数组件的useEffect
的副作用钩子。
接着我们看下面又有一段调用flushPassiveEffects
方法的代码:
js
// finishedWork即是hostFiber
if ((finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||(finishedWork.flags & PassiveMask) !== NoFlags) {
// ... 处理副作用
}
这里的判断条件有两个:
- 判断
HostFiber
节点自身是否存在与PassiveMask
相关的副作用。 - 判断
HostFiber
的子节点是否存在与PassiveMask
相关的副作用。
只要满足任何一个条件,都需要进入内部处理副作用,接着再看具体的处理方法:
js
scheduleCallback(NormalSchedulerPriority,
// 这个回调就是处理useEffect的
() => {
// 处理副作用
flushPassiveEffects();
});
依然是使用scheduleCallback
方法【这个方法的调度逻辑在第二章已经讲述】创建一个新的任务task
,最后会生成一个新的宏任务来异步处理副作用,这里处理副作用的方法就是flushPassiveEffects
函数。
关于
flushPassiveEffects
方法的具体内容会在useEffect
章节中来专门讲解。
关于后面的这个flushPassiveEffects
方法其实很好理解,就是专门用于页面渲染完成后来执行useEffect
的回调钩子,因为commit
阶段的渲染是同步执行,所以通过scheduleCallback
方法调度后,就能保证flushPassiveEffects
方法一定是在本次DOM渲染完成之后执行。
但也正是因为调度的原因,还得保证本次commit
阶段调度的useEffect
必须得在下一次commit
之前执行,所以才会在每一次commit
之前先执行一次flushPassiveEffects
,清理掉可能存在的上一次的useEffect
。
接着从root
应用根节点上取出本次更新创建完成的FiberTree
和更新优先级:
js
const finishedWork = root.finishedWork;
const lanes = root.finishedLanes;
然后立即重置root
节点上的一些状态,方便下次更新任务的内容挂载:
js
// 重置虚拟DOM树和优先级
root.finishedWork = null;
root.finishedLanes = NoLanes;
// 重置回调任务与优先级
root.callbackNode = null;
root.callbackPriority = NoLane;
总结: commit
阶段的准备工作主要就是:清理可能存在的上一次的useEffect
,然后调度本次commit
阶段的useEffect
,最后重置一些基本信息方便下个更新任务的内容挂载。
主要工作
下面我们继续查看主要工作的内容,它是commit
阶段的核心逻辑,这部分内容可以分为三个子阶段:
js
// packages\react-reconciler\src\ReactFiberWorkLoop.new.js
function commitRootImpl() {
...
// 检查HostFiber的子孙元素是存在副作用
const subtreeHasEffects = (finishedWork.subtreeFlags
&(BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !== NoFlags;
// 检查HostFiber自身存在副作用
const rootHasEffect = (finishedWork.flags &(BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
NoFlags;
// 任何一个有副作用,说明需要更新,进入commit逻辑
if (subtreeHasEffects || rootHasEffect) {
# 进入三个子阶段
const prevTransition = ReactCurrentBatchConfig.transition;
ReactCurrentBatchConfig.transition = null;
// 获取当前更新优先级
const previousPriority = getCurrentUpdatePriority();
// 设置同步优先级,commit必须同步执行
setCurrentUpdatePriority(DiscreteEventPriority);
// 当前上下文
const prevExecutionContext = executionContext;
executionContext |= CommitContext;
// 在调用生命周期之前将其重置为null
ReactCurrentOwner.current = null;
// 1,BeforeMutation 【这个阶段执行与flags相关的副作用】
const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
root,
finishedWork,
);
// 2,完成真实的dom渲染,更新页面【这个阶段主要是对DOM的增删改】
commitMutationEffects(root, finishedWork, lanes);
// 渲染后,重置container容器信息
resetAfterCommit(root.containerInfo);
// 根据Fiber双缓冲机制,完成Current Fiber Tree的切换
// 其实也并不一定叫切换,就是将最新work内容存储为当前的内容,下一次的work就可以利用当前的内容
// 注意:到这里是页面已经完成了更新渲染,这个交互Fiber Tree是为了保留最新的Tree,提供给下次更新使用
// 同时也方便调用生命周期钩子时是最新的DOM
root.current = finishedWork;
// 3, Layout阶段【这个阶段会执行一些副作用:比如class组件的mount/update钩子,Current Fiber Tree已经是本次更新的Fiber Tree】
// Layout名称来源于useLayoutEffect,函数组件的useLayoutEffect callback会这个阶段执行
commitLayoutEffects(finishedWork, root, lanes);
} else {
// No effects.
// 本次更新没有副作用, 直接覆盖
root.current = finishedWork;
}
...
}
首先定义了两个状态变量:
subtreeHasEffects
:检查HostFiber
的子孙元素是否存在副作用。rootHasEffect
:检查HostFiber
自身是否存在副作用。
并且这两个变量的判断条件都跟四个mask
掩码相关。
BeforeMutationMask | MutationMask | LayoutMask | PassiveMask
只要满足其中任何一个mask
,则变量结果为true
。表示存在相关的副作用,需要执行commit
逻辑。
在这里我们首先要了解react
源码中一些常规flags
标记对应的场景:
js
// packages\react-reconciler\src\ReactFiberFlags.js
Placement // 代表当前Fiber或者子孙Fiber存在需要插入或者移动的dom元素或者文本【hostComponent,hostText】
Update // 1.触发class组件的mount/update生命周期钩子函数; 2.hostComponent发生属性变化; 3.hostText发生文本变化; 4.Fun组件定义了useLayoutEffect
Deletion // 当前节点存在删除操作
ChildDeletion // 子节点存在需要删除的dom或者文本【hostComponent,hostText】
ContentReset // 清空hostComponent,即DOM节点的文本内容
Callback // 调用this.setState时,传递了回调函数参数
Ref // ref引用的创建与更新
Snapshot // 触发class组件的getSnapshotBeforeUpdate方法【使用较少】
Passive // 触发函数组件的useEffect钩子
知道了这些flags
基本的定义,我们再看这四个mask
的定义:
ini
BeforeMutationMask = Update | Snapshot;
MutationMask = Placement | Update | ChildDeletion | ContentReset | Ref | Hydrating | Visibility;
LayoutMask = Update | Callback | Ref | Visibility;
PassiveMask = Passive | ChildDeletion;
这四个mask
包括了上面的各种flags
,而react应用的加载和更新基本都会存在各种副作用,所以默认都会进入这个判断,然后开始三个commit
子阶段的逻辑执行,下面我们就按执行顺序逐个解析每个阶段执行逻辑。
在讲解之前还有一个处理要注意:
js
// 设置为同步优先级
setCurrentUpdatePriority(DiscreteEventPriority);
设置当前更新的优先级为DiscreteEventPriority
【同步优先级】,代表commit阶段的任务是立即执行的同步任务,并且是不可中断的。这在下面的三个子阶段里面都可以发现这个特点:依然采用递归遍历来处理所有Fiber
节点的副作用,但是循环会持续到工作完成,没有像reconciler
协调流程有可以中断的条件和机会。
1,commitBeforeMutationEffects
js
commitBeforeMutationEffects(root, finishedWork);
前面我们已经知道了BeforeMutationMask
的定义:
ini
BeforeMutationMask = Update | Snapshot;
而BeforeMutationMask
就是表示BeforeMutation
阶段所需要执行的哪些副作用类型:
- 触发class组件的
getSnapshotBeforeUpdate
钩子。 hostComponent
发生属性变化,需要更新执行。hostText
发生文本变化,需要更新执行。
查看commitBeforeMutationEffects
方法:
js
// packages\react-reconciler\src\ReactFiberCommitWork.new.js
export function commitBeforeMutationEffects(
root: FiberRoot,
firstChild: Fiber,
) {
focusedInstanceHandle = prepareForCommit(root.containerInfo);
// 设置第一个处理的节点为 hostFiber
nextEffect = firstChild;
# 提交开始
commitBeforeMutationEffects_begin();
const shouldFire = shouldFireAfterActiveInstanceBlur;
shouldFireAfterActiveInstanceBlur = false;
focusedInstanceHandle = null;
return shouldFire;
}
初始化nextEffect
变量,这里的firstChild
代表第一个处理的节点为hostFiber
。
然后调用commitBeforeMutationEffects_begin
,开始递归遍历FiberTree
,处理有副作用的Fiber
节点。
commitBeforeMutationEffects_begin
查看commitBeforeMutationEffects_begin
方法:
js
// packages\react-reconciler\src\ReactFiberCommitWork.new.js
function commitBeforeMutationEffects_begin() {
# while循环,处理所有节点
while (nextEffect !== null) {
const fiber = nextEffect;
// 取出子节点 刚开始是hostFiber节点的child: 即App 组件
const child = fiber.child;
// 如果该fiber的子节点存在BeforeMutation阶段相关的flgas标记 且 child不为null; 则继续循环,
if ((fiber.subtreeFlags & BeforeMutationMask) !== NoFlags && child !== null) {
child.return = fiber;
// 设置child为下一个处理的节点
nextEffect = child;
} else {
// 只有当fiber的子节点不存在BeforeMutation阶段相关的flgas标记 且 child为null; 【叶子节点】
// 和reconciler流程一样,第一个进入complete工作的都是【div react源码调试】节点
commitBeforeMutationEffects_complete();
}
}
}
在这里我们可以发现,commitBeforeMutationEffects_begin
方法的结构和reconciler
流程中的beginWork
工作逻辑非常相似,唯一的区别就是BeforeMutation
阶段这里的循环并没有中断的条件,必须将所有Fiber
节点都循环处理一遍,才会跳出循环,代表工作结束。
继续查看commitBeforeMutationEffects_begin
方法,每次循环都会将它的child
子节点取出,判断是否满足以下条件:
- 该
fiber
的子节点存在BeforeMutation
阶段相关的flags
标记 。 child
不为null
。
如果同时满足这两个条件,则将child
设置为新的nextEffect
,开启下一个循环。如果不满足这两个条件,则说明当前节点就是需要处理副作用的节点,即需要进入commitBeforeMutationEffects_complete
方法,执行它的complete
工作。
为什么满足条件要继续循环,不满足条件才进入
complete
。因为满足条件代表它的子节点有需要处理的内容,而不是本节点,所以才需要进行下一轮的循环,不满足条件时则代表本节点有需要执行的副作用。
commitBeforeMutationEffects_complete
查看commitBeforeMutationEffects_complete
方法:
js
// packages\react-reconciler\src\ReactFiberCommitWork.new.js
function commitBeforeMutationEffects_complete() {
while (nextEffect !== null) {
const fiber = nextEffect;
try {
// 处理当前节点的副作用
commitBeforeMutationEffectsOnFiber(fiber);
} catch (error) {
captureCommitPhaseError(fiber, fiber.return, error);
}
// 取出兄弟节点,
const sibling = fiber.sibling;
if (sibling !== null) {
sibling.return = fiber.return;
// 将兄弟节点设置为下一个nextEffect 【回到begin工作】
nextEffect = sibling;
return;
}
nextEffect = fiber.return;
}
}
commitBeforeMutationEffects_complete
方法也是一个while
循环,对当前Fiber
节点执行flags
标记对应的操作,即执行commitBeforeMutationEffectsOnFiber
方法。执行完成后,如果当前Fiber
节点存在sibling
兄弟节点,则将兄弟节点设置为最新的nextEffect
,退出当前函数,开启兄弟节点的begin
工作。如果不存在兄弟节点,则将当前Fiber
节点的父级节点设置为最新的nextEffect
,执行父级节点的commitBeforeMutationEffects_complete
工作。
根据上面可以看出,BeforeMutation
阶段逻辑和之前创建FiberTree
的逻辑基本相同,都是深度优先遍历的顺序从HostFiber
根节点开始【自上而下】遍历处理每一个满足条件的Fiber
节点,执行flags
对应的副作用操作,即相似的begin
和complete
工作逻辑。这里主要的执行内容在commitBeforeMutationEffectsOnFiber
方法之中。
注意,其实commit阶段中三个子阶段的逻辑:基本都是以这种逻辑方式来处理相关的副作用内容。
commitBeforeMutationEffectsOnFiber
继续查看commitBeforeMutationEffectsOnFiber
方法:
js
// packages\react-reconciler\src\ReactFiberCommitWork.new.js
function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
const current = finishedWork.alternate;
// 副作用标记
const flags = finishedWork.flags;
// 判断满足flags的节点
if ((flags & Snapshot) !== NoFlags) {
setCurrentDebugFiberInDEV(finishedWork);
// 根据节点的类型,进行不同的副作用处理
switch (finishedWork.tag) {
case ClassComponent: {
if (current !== null) {
const prevProps = current.memoizedProps;
const prevState = current.memoizedState;
const instance = finishedWork.stateNode;
const snapshot = instance.getSnapshotBeforeUpdate(
finishedWork.elementType === finishedWork.type
? prevProps
: resolveDefaultProps(finishedWork.type, prevProps),
prevState,
);
instance.__reactInternalSnapshotBeforeUpdate = snapshot;
}
break;
}
// hostFiber节点的处理
case HostRoot: {
if (supportsMutation) {
// 应用根节点
const root = finishedWork.stateNode;
// 设置textContent = ''; 也就是清空#div容器内容, 方便Mutation阶段的渲染
clearContainer(root.containerInfo);
}
break;
}
}
}
}
进入commitBeforeMutationEffectsOnFiber
方法后,发现只有Snapshot
标记的副作用才会执行。
ini
BeforeMutationMask = Update | Snapshot;
也就是说BeforeMutationMask
虽然包含了两种标记的副作用,但实际只会执行Snapshot
标记的副作用。
BeforeMutation阶段总结
所以BeforeMutation
阶段最终只会执行这两种副作用:
- 触发类组件的
getSnapshotBeforeUpdate
钩子。 - 处理
HostRoot
类型节点【HostFiber
】,清空#root
容器内容, 方便下面Mutation
阶段的DOM挂载。
2,commitMutationEffects
js
commitMutationEffects(root, finishedWork, lanes);
前面我们已经知道了MutationMask
的定义:
js
MutationMask = Placement | Update | ChildDeletion | ContentReset | Ref | Hydrating | Visibility;
而MutationMask
就是代表Mutation
阶段所需要执行的哪些副作用类型,虽然可以看到MutationMask
集成了很多的副作用标记,但是Mutation
阶段的重点内容 :还是针对Fiber
节点上DOM内容的处理,然后将最终的DOM内容渲染到页面。
查看commitMutationEffects
方法:
js
// packages\react-reconciler\src\ReactFiberCommitWork.new.js
export function commitMutationEffects(
root: FiberRoot,
finishedWork: Fiber, // hostFiber
committedLanes: Lanes,
) {
inProgressLanes = committedLanes;
inProgressRoot = root;
// 从hostFiber节点开始处理
commitMutationEffectsOnFiber(finishedWork, root, committedLanes);
inProgressLanes = null;
inProgressRoot = null;
}
直接查看commitMutationEffectsOnFiber
方法,开启Mutation
阶段的逻辑处理。
commitMutationEffectsOnFiber
查看commitMutationEffectsOnFiber
方法:
js
// packages\react-reconciler\src\ReactFiberCommitWork.new.js
function commitMutationEffectsOnFiber(
finishedWork: Fiber,
root: FiberRoot,
lanes: Lanes,
) {
// 旧hostFiber 节点
const current = finishedWork.alternate;
// 取出dom操作的标识flags
const flags = finishedWork.flags;
// 根据节点tag,执行不同的逻辑
switch (finishedWork.tag) {
// 函数组件处理
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
// recursivelyTraverse:递归遍历
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
// app组件跳出了循环,向下继续执行
commitReconciliationEffects(finishedWork);
if (flags & Update) {
try {
commitHookEffectListUnmount(
HookInsertion | HookHasEffect,
finishedWork,
finishedWork.return,
);
commitHookEffectListMount(
HookInsertion | HookHasEffect,
finishedWork,
);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
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);
if (flags & Ref) {
if (current !== null) {
safelyDetachRef(current, current.return);
}
}
return;
}
// 原生dom元素处理
case HostComponent: {
// 递归遍历
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
// 处理dom
commitReconciliationEffects(finishedWork);
if (flags & Ref) {
if (current !== null) {
safelyDetachRef(current, current.return);
}
}
if (supportsMutation) {
// 如果flags标记为ContentReset
if (finishedWork.flags & ContentReset) {
// dom实例
const instance: Instance = finishedWork.stateNode;
try {
// 重置dom的内容
resetTextContent(instance);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
// 如果flgas标记为Update更新
if (flags & Update) {
// 取出当前节点对应的DOM对象
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 {
commitUpdate(
instance,
updatePayload,
type,
oldProps,
newProps,
finishedWork,
);
} catch (error) {
captureCommitPhaseError(
finishedWork,
finishedWork.return,
error,
);
}
}
}
}
}
return;
}
// 文本处理
case HostText: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
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;
}
// hostFiber根节点处理 【第一次会走根节点, 从根节点向下递归渲染dom】
case HostRoot: {
// recursivelyTraverse:递归遍历 【页面显示】
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
if (flags & Update) {
if (supportsMutation && supportsHydration) {
if (current !== null) {
const prevRootState: RootState = current.memoizedState;
if (prevRootState.isDehydrated) {
try {
commitHydratedContainer(root.containerInfo);
} catch (error) {
captureCommitPhaseError(
finishedWork,
finishedWork.return,
error,
);
}
}
}
}
if (supportsPersistence) {
const containerInfo = root.containerInfo;
const pendingChildren = root.pendingChildren;
try {
replaceContainerChildren(containerInfo, pendingChildren);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
}
return;
}
// ...
default: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
return;
}
}
}
commitMutationEffectsOnFiber
方法内容很多,但是它的核心逻辑依然是switch case
结构:根据当前Fiber
节点tag
值,对不同类型节点进行不同的逻辑处理。
要处理的组件类型有很多,我们主要掌握几个常见的组件类型处理逻辑:
FunctionComponent
:函数组件。ClassComponent
:类组件。HostComponent
:DOM节点。HostText
:文本节点。HostRoot
:HostFiber
根节点。
从上面几个组件类型的处理逻辑来看,它们都是一套相同的处理逻辑:
js
// 1,删除DOM
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
// 2,插入DOM
commitReconciliationEffects(finishedWork);
// 3,更新(DOM内容)
Mutation阶段执行顺序
在讲具体的执行逻辑之前,我们先得明白Mutation
阶段的执行顺序,这对我们理解DOM是如何渲染到页面的非常重要。
这里我们将上面的处理逻辑进行简化函数名【映射】,方便我们更好的理解:
js
commitMutationEffectsOnFiber => commit()
recursivelyTraverseMutationEffects 这个方法中使用了循环,并且调用了上面的方法=> for()
// 2,插入DOM
commitReconciliationEffects(finishedWork); // 2,3两部分视为一个内容 => content()
// 3,更新(DOM内容)
所以Mutation
阶段的执行顺序就可以表示为:
js
Fiber顺序: HostFiber => fun App => div.App => ...
// 代码执行顺序
commit
for
commit
for
commit
for
commit
...
content
content
content
执行顺序为从上往下,通过这种递归遍历方式,可以发现HostFiber
虽然是第一个进入commit
逻辑的节点,但是它的content
内容却是最后一个执行,也就是说在HostFiber
的content
内容处理完成之后,即代表完整的DOM
树已经渲染到页面。
实际上最后执行插入到页面的操作是在【fun App】组件节点上执行的,后续会有说明。这部分内容在第三章也有解释。
Mutation
执行顺序图:
recursivelyTraverseMutationEffects
首先查看recursivelyTraverseMutationEffects
方法:
js
// packages\react-reconciler\src\ReactFiberCommitWork.new.js
function recursivelyTraverseMutationEffects(
root: FiberRoot, // root
parentFiber: Fiber, // HostFiber
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);
}
}
}
// 如果子节点树有副作用标记
if (parentFiber.subtreeFlags & MutationMask) {
// 取出子节点
let child = parentFiber.child;
while (child !== null) {
// 开始递归渲染
commitMutationEffectsOnFiber(child, root, lanes);
child = child.sibling;
}
}
}
根据上面的代码可以看出,recursivelyTraverseMutationEffects
方法主要就两个逻辑:
- 判断当前
Fiber
节点是否存在deletions
删除标记,如果存在则循环deletions
,删除子节点对应DOM元素的内容。 - 遍历子树,递归调用
commitMutationEffectsOnFiber
。
这里主要讲解第一点内容删除的逻辑,第二点遍历的内容就是前面Mutation
阶段执行顺序的内容。
具体的删除逻辑就是执行commitDeletionEffects
方法,真实的删除逻辑比较复杂,删除一个DOM元素要考虑很多东西,这里我们不会展开commitDeletionEffects
方法,但还是要了解一下可能会造成的一些副作用影响:
- 执行子树所有组件的
unmount
卸载逻辑。 - 执行子树某些类组件的
componentWillUnmount
方法。 - 执行子树某些函数组件的
useEffect
,useLayoutEffect
等hooks的destory
销毁方法。 - 执行子树所有
ref
属性的卸载操作。
这里将删除DOM 的逻辑放在每个组件的第一个执行,也是非常必要的。因为Mutation
阶段的核心就是DOM操作,如果对应的DOM都已经被删除了,那么也就没有必要再去执行剩下的修改更新了。
commitReconciliationEffects
接着查看commitReconciliationEffects
方法:
js
// packages\react-reconciler\src\ReactFiberCommitWork.new.js
function commitReconciliationEffects(finishedWork: Fiber) {
const flags = finishedWork.flags;
// 如果是插入/移动标记
if (flags & Placement) {
try {
// 执行dom插入添加操作
commitPlacement(finishedWork);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
finishedWork.flags &= ~Placement;
}
if (flags & Hydrating) {
finishedWork.flags &= ~Hydrating;
}
}
commitReconciliationEffects
方法就是执行DOM的插入或者移动操作,判断当前Fiber
节点是否存在Placement
标记,存在就会执行commitPlacement
方法,执行相关的DOM的操作。
commitPlacement
继续查看commitPlacement
方法:
js
// packages\react-reconciler\src\ReactFiberCommitWork.new.js
function commitPlacement(finishedWork: Fiber): void {
if (!supportsMutation) {
return;
}
// finishedWork: fun App 它的父级节点为HostFIber
const parentFiber = getHostParentFiber(finishedWork);
switch (parentFiber.tag) {
# 普通DOM节点
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;
}
# 处理HostFiber节点的插入动作:
case HostRoot:
case HostPortal: {
// #div
const parent: Container = parentFiber.stateNode.containerInfo;
// 无兄弟节点
const before = getHostSibling(finishedWork);
# 将离屏的DOM树插入到#div; 马上页面上显示出DOM内容
insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
break;
}
}
}
能够执行Placement
副作用的,只有两种组件节点HostComponent
和HostRoot
。
对于HostComponent
来说,就是常规的DOM添加和插入,即调用原生的DOM方法执行对应的逻辑:
js
// 原生DOM操作
parentNode.appendChild()
parentNode.insertBefore()
首屏渲染
注意: 在mount
阶段,也就是react应用的首次加载时,只会执行一次Placement
插入操作,这个执行对象是【App根组件】对应的Fiber
节点。这个是reconciler
协调流程中设置的,可以查看第三章关于此部分的逻辑,我们再来回顾一下:
js
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes,
) {
if (current === null) {
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes,
);
} else {
// 在Mount阶段,只有hostFiber进入这个逻辑
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes,
);
}
}
在react应用的首次加载时【mount
阶段】,只有HostFiber
节点进入了else
分支【else分支默认对应的是update
阶段】,因为HostFiber
节点所对应current
始终是存在的。
js
export const reconcileChildFibers = ChildReconciler(true);
这里ChildReconciler
调用时传入了true
的状态,这个参数就是下面的shouldTrackSideEffects
。
所以HostFiber
在创建它的child
:也就是App根组件所对应的节点时【newFiber
】,就被打上了Placement
插入标记。
js
// place插入操作
function placeSingleChild(newFiber) {
// true && 新建的Fiber节点alternate都是null newFiber是App根组件的fiber
if (shouldTrackSideEffects && newFiber.alternate === null) {
newFiber.flags |= Placement;
}
return newFiber;
}
mount
阶段只有App根组件对应的Fiber
节点存在插入标记,这样的好处是:在react应用的首次加载时【首屏渲染】,无需执行大量的插入操作,只需执行一次插入操作,即可将已经构建完成的离屏DOM树加载到页面。
这里我们可以通过源码调试来验证:
此时对应的控制台输出:
这里输出的Placement
标记对应的Fiber
节点就是:App根组件节点。因为commitPlacement
方法中的判断是取的父级的tag
值,App根组件的父级节点就是HostFiber
,就会进入hostRoot
分支逻辑:
js
function commitPlacement(finishedWork: Fiber): void {
// finishedWork: fun App 它的父级节点为HostFIber
const parentFiber = getHostParentFiber(finishedWork);
switch (parentFiber.tag) {
...
// 处理HostFiber节点的插入动作:
case HostRoot:
case HostPortal: {
// #root
const parent = parentFiber.stateNode.containerInfo;
// 无兄弟节点
const before = getHostSibling(finishedWork);
# 将离屏的DOM树插入到#root; 页面上立即就显示出DOM内容
insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
break;
}
}
}
这里的parent
即是#root
容器节点,App根组件没有兄弟节点,before
为null
。
进入insertOrAppendPlacementNodeIntoContainer
方法:
js
function insertOrAppendPlacementNodeIntoContainer(
node: Fiber,
before: ?Instance,
parent: Container,
): void {
const {tag} = node; // App根组件节点
// Host 或者 text
const isHost = tag === HostComponent || tag === HostText;
if (isHost) {
const stateNode = node.stateNode;
if (before) {
insertInContainerBefore(parent, stateNode, before);
} else {
appendChildToContainer(parent, stateNode);
}
} else if (tag === HostPortal) {
...
} else {
# fun App节点的child ,也就是div.App 这个节点的内容就是一个离屏的DOM树
const child = node.child;
if (child !== null) {
# 将整个离屏的DOM树添加到 #div中,页面渲染完成
insertOrAppendPlacementNodeIntoContainer(child, before, parent);
}
}
}
在进入insertOrAppendPlacementNodeIntoContainer
方法后,又是根据节点类型进行不同处理,App根组件节点不满足上面的类型,只能进入else
分支。然后取出App根组件节点的child
,也就是【div.app】所对应的Fiber
节点。
js
<div className="App">
...
</div>
因为函数组件节点是不会存储DOM结构的,只能向下寻找。App组件是根组件,它的根DOM
就是当前DOM树的根DOM元素。
此时它的DOM
内容就是已经构建完成的离屏DOM树:
可以看见此时的页面还是空白内容,但当我们一执行insertOrAppendPlacementNodeIntoContainer
方法:
js
// 相当于执行
#root .appendChild(div.App)
这棵完整的DOM树就会被立即渲染到页面上:
Mutation
阶段的核心逻辑就相当于基本执行完成了。
解析完了Mutation
阶段整体的加载流程,下面我们再看每个组件节点一些不同的处理:
- 函数组件:处理Effect hook相关的副作用。
js
// 循环effect链表执行effect.destory方法
commitHookEffectListUnmount();
// 循环effect链表执行effect.create方法
commitHookEffectListMount();
关于Effect hook的细节处理会放到useEffect章节中展开讲解。
- 类组件:更新
ref
引用。
js
if (flags & Ref) {
if (current !== null) {
// 更新ref
safelyDetachRef(current, current.return);
}
}
HostComponent
:关于DOM节点的逻辑处理最多。更新ref
引用和重置dom
内容,最后更新dom的属性和样式。
js
// 1,更新ref
if (flags & Ref) {
if (current !== null) {
safelyDetachRef(current, current.return);
}
}
// 2,重置dom内容
if (finishedWork.flags & ContentReset) {
// dom实例
const instance: Instance = finishedWork.stateNode;
// 重置dom内容
resetTextContent(instance);
}
// 3,更新dom属性和样式
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);
commitUpdate(instance, updatePayload, type, oldProps, newProps, finishedWork);
}
}
HostText
:更新文本内容。
js
if (flags & Update) {
const textInstance: TextInstance = finishedWork.stateNode;
const newText: string = finishedWork.memoizedProps;
const oldText: string = current !== null ? current.memoizedProps : newText;
// 更新文本内容
commitTextUpdate(textInstance, oldText, newText);
}
Mutation阶段总结
Mutation
阶段的主要工作是对DOM内容的增删改操作,最后将构建完成的离屏DOM树渲染到页面。
3,commitLayoutEffects
js
commitLayoutEffects(finishedWork, root, lanes);
前面我们已经知道了LayoutMask
的定义:
js
LayoutMask = Update | Callback | Ref | Visibility;
而LayoutMask
就是代表Layout
阶段所需要执行的哪些副作用类型:
- 类组件的
componentDidMount/componentDidUpdate
生命周期钩子函数的执行。 - 类组件调用
this.setState
时传递的callback
回调函数的会被保存到Fiber
节点的updateQueue
属性中在这里执行。 - 执行函数组件的
useLayoutEffect hook
回调。
可以说Layout
阶段的主要内容:就是在DOM渲染完成后 ,执行函数组件和类组件定义的一些callback
回调函数。
查看commitLayoutEffects
方法:
js
// packages\react-reconciler\src\ReactFiberCommitWork.new.js
export function commitLayoutEffects(
finishedWork: Fiber,
root: FiberRoot,
committedLanes: Lanes,
): void {
inProgressLanes = committedLanes;
inProgressRoot = root;
// HostFiber
nextEffect = finishedWork;
# 又是begin 和complete工作逻辑
# 相当于递归循环触发每个组件的生命周期钩子函数 以及相关的hooks回调
commitLayoutEffects_begin(finishedWork, root, committedLanes);
inProgressLanes = null;
inProgressRoot = null;
}
可以看出Layout
阶段的处理逻辑还是begin
和complete
工作,继续以深度优先遍历的顺序,根据不同的组件类型,进行不同的逻辑处理,这里就不重复描述了,下面就直接贴出相关的代码。
在这里我们也可以看出,react源码中创建FiberTree
的reconciler
协调流程和commit
阶段的渲染流程都是基本相同的工作逻辑:
- 深度优先遍历
FiberTree
。 beginWork
和completeWork
工作逻辑。wrokInProgree.tag
根据不同的节点类型进行不同的处理。
只要你理解这一套运行逻辑:你就掌握了react源码中最核心的工作逻辑。
commitLayoutEffects_begin
js
// packages\react-reconciler\src\ReactFiberCommitWork.new.js
function commitLayoutEffects_begin(
subtreeRoot: Fiber,
root: FiberRoot,
committedLanes: Lanes,
) {
// Suspense layout effects semantics don't change for legacy roots.
const isModernRoot = (subtreeRoot.mode & ConcurrentMode) !== NoMode;
// 循环触发
while (nextEffect !== null) {
// HostFiber
const fiber = nextEffect;
// fun App组件节点
const firstChild = fiber.child;
# 说明子树存在副作用,需要更新nextEffect为子节点,进入下一级的循环
if ((fiber.subtreeFlags & LayoutMask) !== NoFlags && firstChild !== null) {
firstChild.return = fiber;
nextEffect = firstChild;
} else {
# 说明副作用在自身节点了,进入complete阶段
commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes);
}
}
}
可以看出这里的逻辑结构和BeforeMutation
阶段完全一致,区别只是处理的内容不同。
commitLayoutMountEffects_complete
查看commitLayoutMountEffects_complete
方法:
js
// packages\react-reconciler\src\ReactFiberCommitWork.new.js
function commitLayoutMountEffects_complete(
subtreeRoot: Fiber,
root: FiberRoot,
committedLanes: Lanes,
) {
// while循环
while (nextEffect !== null) {
const fiber = nextEffect;
// 存在layout相关的副作用 才会进入
if ((fiber.flags & LayoutMask) !== NoFlags) {
const current = fiber.alternate;
setCurrentDebugFiberInDEV(fiber);
try {
# 执行副作用
commitLayoutEffectOnFiber(root, current, fiber, committedLanes);
} catch (error) {
captureCommitPhaseError(fiber, fiber.return, error);
}
}
# 当回到HostFiber时,代表循环完成,退出layout工作
if (fiber === subtreeRoot) {
nextEffect = null;
return;
}
# 存在兄弟节点,开始兄弟节点的工作
const sibling = fiber.sibling;
if (sibling !== null) {
sibling.return = fiber.return;
nextEffect = sibling;
return;
}
# 不存在兄弟节点,则返回父级节点
nextEffect = fiber.return;
}
}
依然是和BeforeMutation
完全一致的循环逻辑,可以对比查看。
commitLayoutEffectOnFiber
继续查看commitLayoutEffectOnFiber
方法内容:
js
// packages\react-reconciler\src\ReactFiberCommitWork.new.js
function commitLayoutEffectOnFiber(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
committedLanes: Lanes,
): void {
if ((finishedWork.flags & LayoutMask) !== NoFlags) {
# 根据组件类型:进行不同的处理
switch (finishedWork.tag) {
# 1,函数组件的处理
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
if (!enableSuspenseLayoutEffectSemantics || !offscreenSubtreeWasHidden) {
// 处理副作用
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
}
break;
}
# 2,类组件的处理
case ClassComponent: {
// 组件实例
const instance = finishedWork.stateNode;
if (finishedWork.flags & Update) {
if (!offscreenSubtreeWasHidden) {
if (current === null) {
// mount加载阶段
# 触发componentDidMount 生命周期钩子函数【这类静态方法:是存储在instance对象原型上的】
instance.componentDidMount();
} else {
// update更新阶段
const prevProps = finishedWork.elementType === finishedWork.type ? current.memoizedProps
: resolveDefaultProps( finishedWork.type, current.memoizedProps);
const prevState = current.memoizedState;
# 触发componentDidUpdate 生命周期钩子函数
instance.componentDidUpdate( prevProps,prevState,
instance.__reactInternalSnapshotBeforeUpdate,
);
}
}
}
# 取出当前组件节点的updateQueue更新对象
const updateQueue: UpdateQueue = finishedWork.updateQueue;
if (updateQueue !== null) {
// 触发更新
commitUpdateQueue(finishedWork, updateQueue, instance);
}
break;
}
case HostRoot: {
// TODO: I think this is now always non-null by the time it reaches the
// commit phase. Consider removing the type check.
const updateQueue: UpdateQueue = finishedWork.updateQueue;
if (updateQueue !== null) {
let instance = null;
if (finishedWork.child !== null) {
switch (finishedWork.child.tag) {
case HostComponent:
instance = getPublicInstance(finishedWork.child.stateNode);
break;
case ClassComponent:
instance = finishedWork.child.stateNode;
break;
}
}
// 触发更新
commitUpdateQueue(finishedWork, updateQueue, instance);
}
break;
}
case HostComponent: {
// 取出DOM对象
const instance: Instance = finishedWork.stateNode;
if (current === null && finishedWork.flags & Update) {
const type = finishedWork.type;
const props = finishedWork.memoizedProps;
# 针对一些特殊的DOM元素做加载处理:button,input等做自动聚焦,对Img图片做加载
commitMount(instance, type, props, finishedWork);
}
break;
}
...
}
}
}
commitLayoutEffectOnFiber
方法依然是和前面两个阶段的逻辑一样,根据不同的组件节点进行不同的逻辑处理,这里我们还是理解几个重点组件类型即可。
- 函数组件
js
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
执行commitHookEffectListMount
方法,循环触发当前函数组件的useLayoutEffect
回调。
- 类组件
根据当前Fiber
节点是否存在current
【旧的Fiber
】来区分是mount
阶段还是update
阶段:
mount
阶段:执行当前类组件的componentDidMount
生命周期钩子函数。update
阶段:执行当前类组件的componentDidUpdate
生命周期钩子函数。
最后执行当前类组件调用this.setState
时传入的callback
回调函数。
HostComponent
针对一些特殊的DOM元素做加载处理:比如input
输入框做自动聚焦,对Img
图片做加载。
Layout阶段总结
在真实DOM加载完成后,执行函数组件的Layout hook
回调和类组件的生命周期钩子及setState
相关回调。
4,FiberTree的切换
js
// 2,Mutation阶段
commitMutationEffects(root, finishedWork, lanes);
// FiberTree的切换
root.current = finishedWork;
// 3, Layout阶段
commitLayoutEffects(finishedWork, root, lanes);
在Mutation
阶段和Layout
阶段之间还有一个重要处理没有说明,那就是FiberTree
的切换。
通过前面我们已经知道,Mutation
阶段处理完成之后,页面就已经完成了真实的DOM渲染。所以此时finishedWork
就是最新的FiberTree
以及存储着最新的DOM内容,在这里更新current
的内容,主要有两个方面的作用:
- 保留最新的
Fiber
树结构,在下一次更新时就可以利用本次的FiberTree
来做数据的复用以及差异对比。 - 对于类组件来说:当在
Layout
阶段执行componentDidMount
或者componentDidUpdate
生命周期钩子时就可以获取最新的DOM内容。
结束语
到此为止,一个react应用的整体加载流程算是解析完成了,其中有一些细节逻辑并没有展开讲解,比如类组件,函数组件的具体加载过程,hooks原理等等。所以后面新的章节会专注于这些细节逻辑的解析,觉得有帮助的朋友也可以继续多多关注。