前言
随着 render
阶段的完成,也意味着在内存中构建 workInProgress Fiber
树的所有工作都已经完成,这其中包括了对 Fiber 节点的 update
、diff
、flags 标记
、subtreeFlags
(effectList) 的收集等操作 我们知道,在 render
阶段,会将需要更新的节点标记上 flags
(effectTag),在 completeWork
阶段会形成 effectList
链表,连接所有需要被更新的节点。
为了将这些需要更新的节点应用到真实 DOM 上却不需要遍历整棵树,在 commit
阶段,会通过遍历这条 EffectList
链表,执行对应的操作,来完成对真实 DOM 的更新,这也叫做 mutation
,即 DOM 节点的增删改操作。
在新版本中不再需要 effectList 链表了,而是通过 rootFiber 自下而上调和的方式处理这些标志,执行对应的操作,来完成对真实 DOM 的更新
接下来我们带着以下的问题一起去思考 commit 阶段的工作!
- commit 阶段分为几个子阶段,都做了什么事情?
- useEffect 钩子是如何被调度的?
commit
阶段会做以下这些事情
- 对一些生命周期和副作用钩子的处理,比如 类组件的
componentDidMount
、componentDidUpdate
,函数组件的useEffect
、useLayoutEffect
、useInsertionEffect
等 - 另一方面,在一次 Update 中,进行添加节点(
Placement
)、更新节点(Update
)、删除节点(Deletion
)、同时有对ref
的处理等。
commit
阶段的入口在 commitRoot
函数,在这里会发起一个最高优先级的调度任务,然后调用 commitRootImpl
函数来处理副作用,将最新的 Fiber 树同步到 DOM 上
function commitRoot(root) {
const previousUpdateLanePriority = getCurrentUpdatePriority();
const prevTransition = ReactCurrentBatchConfig.transition;
try {
ReactCurrentBatchConfig.transition = 0;
setCurrentUpdatePriority(DiscreteEventPriority); // 最高优先级调度
commitRootImpl(root, previousUpdateLanePriority); // commit 主流程
} finally {
// 重置
ReactCurrentBatchConfig.transition = prevTransition;
setCurrentUpdatePriority(previousUpdateLanePriority);
}
return null;
}
流程概览
commit
阶段主要针对 rootFiber
上的 effectList
进行处理,根据对 DOM 的操作时机可以分为三个子阶段
-
Before mutation
阶段(执行 DOM 操作前):读取组件变更前的状态- 对于 CC 而言,会执行
getSnapshotBeforeUpdate
,获取 DOM 更新前的组件实例信息(更新前) - 对于 FC 而言,会异步调度
useEffect
钩子
- 对于 CC 而言,会执行
-
mutation
阶段(执行 DOM 操作):- 对于
HostComponent
会执行相应的 DOM 操作 - 对于 CC 会调用
componentWillUnmount
- 对于 FC 会执行
useLayoutEffect
的销毁函数
- 对于
-
layout
阶段(执行 DOM 操作后):在 DOM 操作完成后,读取当前组件的状态(更新后)
- 对于 CC ,会调用 `componentDidMount` 和 `componentDidUpdate` 以及 `setState` 的回调函数
- 对于 FC ,会执行 `useLayoutEffect` 的回调函数
在这当中,需要注意的是,在 mutation
阶段结束后,layout
开始之前,workInProgress
树会切换成 current
树。这样做是为了
- 在
mutation
阶段调用类组件的componentWillUnmount
的时候, 可以获取到卸载前的组件信息 - 在
layout
阶段调用componentDidMount/Update
时,获取的组件信息是组件更新后的。
commit
阶段的主流程在 commitRootImpl
这个函数中,可以明确的看到三个子阶段的执行
function commitRootImpl(root, renderPriorityLevel) {
// NOTE: 采用 do while 的作用是,在 useEffect 内部可能会触发新的更新,新的更新可能会触发新的副作用 ,因此需要不断的循环,直到 为 null
do {
flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null); // Note:这一步是为了看看还有没有没有执行的 useEffect, 有的话先执行他们
...
const finishedWork = root.finishedWork; // 当前的 rootFiber
const lanes = root.finishedLanes; // 优先级相关
...
root.finishedWork = null;
root.finishedLanes = NoLanes;
...
// 绑定 scheduler 的回调函数
root.callbackNode = null;
root.callbackPriority = NoLane;
let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes);
markRootFinished(root, remainingLanes);
// Note:处理光标,重置一些 render 阶段使用的变量
...
// 子树是否有更新
const subtreeHasEffects =
(finishedWork.subtreeFlags &
(BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
NoFlags;
const rootHasEffect =
(finishedWork.flags &
(BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
NoFlags;
if (subtreeHasEffects || rootHasEffect) {
// 存在副作用,处理 Fiber 上的副作用
...
// 第一个阶段是 before mutation ,在这个阶段可以读取改变之前的的 state
// 生命周期函数 getSnapshotBeforeUpdate 的调用
const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
root,
finishedWork,
);
...
// mutation 阶段,可以在这个阶段 改变 host tree
commitMutationEffects(root, finishedWork, lanes);
...
// 交换 workInProgress
root.current = finishedWork;
...
// 执行 layout
commitLayoutEffects(finishedWork, root, lanes);
...
requestPaint();
// 重置执行栈环境
executionContext = prevExecutionContext;
// 将优先级重置为之前的 非同步优先级
setCurrentUpdatePriority(previousPriority);
ReactCurrentBatchConfig.transition = prevTransition;
} else {
// No effects.
...
}
...
// Note:commit 阶段结尾,可能会在 commit 阶段产生新的更新,因此在 commit 阶段的结尾会重新调度一次
ensureRootIsScheduled(root, now());
...
// Note:react 中会将同步任务放在 flushSync 队列中,执行这个函数会执行它里面的同步任务
// Note:默认 react 中开启的是 legacy 模式,这种模式下的更新都是 同步的 更新,未来会开启 concurrent 模式(并发模式),会出现不同优先级的更新
flushSyncCallbacks();
...
return null;
}
接下来我们去看看每个阶段都分别做了哪些工作!
BeforeMutation 阶段
首先是 before mutation 阶段,在 before mutation 阶段,会执行 commitBeforeMutationEffects
函数,因为此时还没有对真实 DOM 进行修改,因此是获取 DOM 快照的最佳时期,同时也会在此异步调用 useEffect
- 执行
commitBeforeMutationEffectsOnFiber
函数 - DOM 组件的
blur
和focus
事件相关 - 对于类组件,执行
getSnapshotBeforeUpdate
生命周期函数 - 如果 FC 中使用到的
useEffect
,会通过scheduleCallback
来调度passiveEffect
异步执行
passiveEffect 就是 useEffect 对应的 effectTag
beforemutation
阶段的主要控制函数在于 commitBeforeMutationEffects
,主要做的事情就是初始化全局变量 nextEffect
以及 focusedInstanceHandle
,然后调用 commitBeforeMutationEffects_begin
来处理副作用
exportfunction commitBeforeMutationEffects(
root: FiberRoot,
firstChild: Fiber,
) {
focusedInstanceHandle = prepareForCommit(root.containerInfo);
nextEffect = firstChild;
// NOTE:开始执行,
commitBeforeMutationEffects_begin();
// 不再跟踪fiber节点
const shouldFire = shouldFireAfterActiveInstanceBlur;
shouldFireAfterActiveInstanceBlur = false;
focusedInstanceHandle = null;
return shouldFire;
}
准备工作
首先会执行 prepareForCommit
函数,调用 getClosestInstanceFromNode
方法,获取当前节点最近的 HostComponent
或 HostText
类型对应的 Fiber 节点,来初始化全局变量 focusedInstanceHandle
,用来处理 focus 状态
exportfunction prepareForCommit(containerInfo: Container): Object | null {
eventsEnabled = ReactBrowserEventEmitterIsEnabled();
selectionInformation = getSelectionInformation();
let activeInstance = null;
if (enableCreateEventHandleAPI) {
const focusedElem = selectionInformation.focusedElem;
if (focusedElem !== null) {
activeInstance = getClosestInstanceFromNode(focusedElem);
}
}
ReactBrowserEventEmitterSetEnabled(false);
return activeInstance;
}
commitBeforeMutationEffects_begin
在 commitBeforeMutationEffects_begin
函数中会从上往下遍历,找到最底部并且有标记了 before mutation 的 fiber 节点,调用 commitBeforeMutationEffects_complete
函数来更新 props 和 state
如果当前的 Fiber 节点上的 deletions
字段被标记了值,意味着节点即将被删除,会调用 commitBeforeMutationEffectsDeletion
来创建 blur 事件并进行派发
因此可以知道 begin 流程主要做了两件事
-
如果子代 Fiber 树上有 before mutation 标记,会把 nextEffect 赋值给子 Fiber,也就是向下递归找到有标记 before mutation 的 Fiber
-
找到后,执行
commitBeforeMutationEffects_complete
函数
从 commitBeforeMutationEffects_begin
的执行上,我们可以知道:commit 阶段执行的生命周期以及钩子函数是子先后父的
这是因为,如果在子组件中的生命周期内改变 DOM 状态,并且还要在父组件生命周期中同步状态,就需要子先后父之行生命周期
function commitBeforeMutationEffects_begin() {
while (nextEffect !== null) {
const fiber = nextEffect;
// This phase is only used for beforeActiveInstanceBlur.
// Let's skip the whole loop if it's off.
if (enableCreateEventHandleAPI) {
const deletions = fiber.deletions;
if (deletions !== null) {
for (let i = 0; i < deletions.length; i++) {
const deletion = deletions[i];
// 调用 dispatchBeforeDetachedBlur() 来创建 blur 事件并派发
commitBeforeMutationEffectsDeletion(deletion);
}
}
}
const child = fiber.child;
if (
(fiber.subtreeFlags & BeforeMutationMask) !== NoFlags &&
child !== null
) {
ensureCorrectReturnPointer(child, fiber);
nextEffect = child;
} else {
// 更新fiber节点的 props 和 state
commitBeforeMutationEffects_complete();
}
}
}
commitBeforeMutationEffectsOnFiber
在 commitBeforeMutationEffects_begin
中会调用 commitBeforeMutationEffects_complete
函数,在 commitBeforeMutationEffects_complete
中会从下到上归并,(sibling 到 parent)执行 commitBeforeMutationEffectsOnFiber
函数,这也是 before_mutation
的核心逻辑
- 首先会处理 blur 和 focus 相关逻辑
- 其次会执行 getSnapshotBeforeUpdate 的生命周期函数
会根据 Fiber 节点 tag 的不同进入不同的处理逻辑,同时会根据 current
是否存在来判断是 mount 还是 update 阶段,进入不同的处理逻辑
对于 CC 而言,最重要的就是触发生命周期函数,获取当前 DOM 的数据信息
function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
const current = finishedWork.alternate;
const flags = finishedWork.flags;
...
if ((flags & Snapshot) !== NoFlags) {
...
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
break;
}
case ClassComponent: {
if (current !== null) {
// 非首次渲染的情况
// 获取上一次的props
const prevProps = current.memoizedProps;
// 获取上一次的 state
const prevState = current.memoizedState;
// 获取当前 class组件实例
const instance = finishedWork.stateNode;
// 更新 props 和 state
...
// 调用 getSnapshotBeforeUpdate 生命周期方法
const snapshot = instance.getSnapshotBeforeUpdate(
finishedWork.elementType === finishedWork.type
? prevProps
: resolveDefaultProps(finishedWork.type, prevProps),
prevState,
);
...
// 将生成的 snapshot 保存到 instance.__reactInternalSnapshotBeforeUpdate 上
// 供 DidUpdate 生命周期使用
instance.__reactInternalSnapshotBeforeUpdate = snapshot;
}
break;
}
...
}
...
}
}
从 React 16 版本开始,componentWillXX
生命周期函数加上了 UNSAFE_
的前缀,这是因为 Reconciler
重构为 Fiber Reconciler
后,render
阶段执行的任务可能会因为某些特殊原因(有优先级更高任务)会被中断或者是重新开始,对应的组件在 render
阶段的生命周期钩子(即 componentWillXX
)可能会有触发多次的情况,因此加上了 UNSAFE_
前缀,减少使用
而新增的 getSnapShotBeforeUpdate
生命周期函数,它是在 commit
阶段内的 before mutation
阶段调用的,由于 commit
阶段是同步执行的,所以不会遇到多次调用的情况。
调度 useEffect
这一部分在
commitBeforeMutationEffects
函数执行之前,也属于 before mutation 阶段
对于 useEffect
,会通过 scheduler
模块提供的 scheduleCallback
进行调度,用来以某个优先级异步调度一个回调函数。
// 调度 useEffect
if (
(finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
(finishedWork.flags & PassiveMask) !== NoFlags
) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
pendingPassiveEffectsRemainingLanes = remainingLanes;
scheduleCallback(NormalSchedulerPriority, () => {
// 触发 useEffect
flushPassiveEffects();
returnnull;
});
}
}
在此处,被异步调度的回调函数就是触发 useEffect
的方法 flushPassiveEffects
,这个回调函数会在调度后执行,相当于在这里注册了这个回调函数。
所以整个 useEffect
异步调用分为三步:
- before mutation 阶段在
scheduleCallback
中调度flushPassiveEffects
layout
阶段之后将effectList
赋值给rootWithPendingPassiveEffects
scheduleCallback
触发flushPassiveEffects
,flushPassiveEffects
内部遍历rootWithPendingPassiveEffects
在 React 官方文档中,也对 useEffect 的执行时机做出了解释
与
componentDidMount
、componentDidUpdate
不同的是,传给useEffect
的函数会在浏览器完成布局与绘制之后,在一个延迟事件中被调用。这使得它适用于许多常见的副作用场景,比如设置订阅和事件处理等情况,因为绝大多数操作不应阻塞浏览器对屏幕的更新。
此外,从 React 18 开始,当它是离散的用户输入(如点击)的结果时,或者当它是由 flushSync 包装的更新结果时,传递给 useEffect
的函数将在屏幕布局和绘制之前同步执行。这种行为便于事件系统或 flushSync 的调用者观察该效果的结果。少用!!少用!!会影响性能
下面我们来看看 mutation 阶段的工作
Mutation 阶段
上面讲了 beforeMutation
阶段的工作,接下来到了执行 DOM 操作的 mutation
阶段的工作。
在 before mutation
阶段中,会一上一下的之行 begin 和 complete 的工作,最后 nextEffect 又回到了起始点
mutation
阶段会用同样的方式,向下遍历,向上归并,执行对应的函数,这里执行的是 commitMutationEffects
函数,它会通过调用 commitMutationEffects_begin
函数来开始本次的 mutation
阶段的工作
React 将每一个阶段又分为了 begin 和 complete,这样将逻辑进行抽离,主函数流程更加清晰
exportfunction commitMutationEffects(
root: FiberRoot,
firstChild: Fiber,
committedLanes: Lanes,
) {
inProgressLanes = committedLanes; // 优先级相关
inProgressRoot = root;
nextEffect = firstChild;
commitMutationEffects_begin(root);
inProgressLanes = null;
inProgressRoot = null;
}
commitMutationEffects_begin 入口
可以看到在这个函数中,主体是一个 while
循环,会从 rootFiber 开始向下遍历,和 before mutation 的工作一样,找到最底层的有 mutation 标志的 fiber 节点,执行 commitMutationEffects_complete
函数
如果遍历到的 Fiber 上有 Deletion 标记,则调用 commitDeletion
函数,分离 ref 引用,并调用 componentWillUnmount
生命周期函数,断开 Fiber 与父节点的连接关系。这些工作都在 commitDeletion
函数中进行处理
这是在 React 17.0.3 之后才启用的字段,会在需要被 delete 掉的 Fiber 节点上的 deletions 字段上打上标记,这样可以直接通过 deletions 字段来判断是否需要删除该节点
function commitMutationEffects_begin(root: FiberRoot) {
while (nextEffect !== null) {
const fiber = nextEffect;
const deletions = fiber.deletions;
if (deletions !== null) {
for (let i = 0; i < deletions.length; i++) {
const childToDelete = deletions[i];
try {
// 断开当前 Fiber 节点与 父节点之间的连接
// 分离 ref ,调用 componentWillUnmount
commitDeletion(root, childToDelete, fiber);
} catch (error) {
...
}
}
}
const child = fiber.child;
// ... 省去判断逻辑 nextEffect = child;
commitMutationEffects_complete(root);
}
}
commitMutationEffects_complete
在 commitMutationEffects_complete
函数中,会开始归并,优先处理兄弟节点,最后处理父节点,调用 commitMutationEffectsOnFiber
函数,根据不同的组件类型,来执行更新、插入、删除 DOM 的操作
function commitMutationEffects_complete(root: FiberRoot) {
while (nextEffect !== null) {
const fiber = nextEffect;
...
// 核心,根据不同的类型,进行处理
commitMutationEffectsOnFiber(fiber, root);
...
const sibling = fiber.sibling;
if (sibling !== null) {
ensureCorrectReturnPointer(sibling, fiber.return);
nextEffect = sibling;
return;
}
nextEffect = fiber.return;
}
}
commitMutationEffectsOnFiber
在 commitMutationEffectsOnFiber
函数中
- 首先会判断是否需要重置文本节点
- 然后判断是否有
ref
的更新 - 然后会根据 Fiber 上的
flags
的类型进行二进制计算,根据计算结果来执行不同的操作逻辑,这和前面介绍的effectTag
的计算是相同的。会有多个 case 存在
- Placement:执行
commitPlacement
函数插入 DOM 节点,然后删除 Placement 的 effectTag - Update:执行
commitWork
函数来执行更新操作,然后删除 Update 的 effectTag - PlacementAndUpdate:先调用
commitPlacement
执行插入操作,然后再调用commitWork
执行更新操作。
对于 Deletion 的操作已经前置处理了,这里不介绍
function commitMutationEffectsOnFiber(finishedWork: Fiber, root: FiberRoot) {
const flags = finishedWork.flags;
// 判断是否存在 文本节点,重置文本节点
if (flags & ContentReset) {
commitResetTextContent(finishedWork);
}
if (flags & Ref) {
const current = finishedWork.alternate;
if (current !== null) {
commitDetachRef(current);
}
if (enableScopeAPI) {
if (finishedWork.tag === ScopeComponent) {
commitAttachRef(finishedWork);
}
}
}
// ... 处理副作用
const primaryFlags = flags & (Placement | Update | Hydrating);
outer: switch (primaryFlags) {
case Placement: {
commitPlacement(finishedWork);
finishedWork.flags &= ~Placement;
break;
}
case PlacementAndUpdate: {
// Placement
commitPlacement(finishedWork);
finishedWork.flags &= ~Placement;
// Update
const current = finishedWork.alternate;
commitWork(current, finishedWork);
break;
}
// SSR 相关 case
...
case Update: {
const current = finishedWork.alternate;
commitWork(current, finishedWork);
break;
}
}
}
接下来我们来看看相应的对真实 DOM 节点的操作是如何进行的
Placement 插入节点
当 flags
包含 Placement
的 effectTag
时,会调用这个 commitPlacement
函数来执行对 DOM 节点的插入操作
主要的思路是
- 首先会根据当前的 Fiber 节点,来找到离他最近的 Host 类型的 Parent Fiber 节点
Host 类型包括:HostComponent、HostRoot ...
- 然后根据
parent
Fiber 节点的tag
类型,来判断父 Fiber 节点对应的 DOM 节点是否可以作为container
容器,因为父节点有可能是一个component
这样就不能直接插入 - 当找到
parent Fiber
之后,如果parent Fiber
上存在contentReset
的effectTag
,就需要执行resetTextContent
,来重置文本 - 接下来会找到当前
Fiber
节点的 Host 类型的slibing
节点
- 当执行
insertBefore
时,就需要知道当前 Fiber 节点对应的兄弟节点 - 当需要执行
appendChild
时,需要知道当前 Fiber 节点的 Host 类型 Parent 节点
- 根据是否可以作为
container
,来调用不同的函数在指定的位置插入新的节点。实际上这两个函数的处理逻辑是一致的,唯一的区别就是需不需要判断父节点是不是COMMENT_NODE
function commitPlacement(finishedWork: Fiber): void {
// NOTE:如果不支持 mutation 会直接返回了
if (!supportsMutation) {
return;
}
// NOTE:根据当前节点找到离他最近的 host 类型 fiber 节点
// getHostParentFiber 一直向上递归查找,直到找到为止
const parentFiber = getHostParentFiber(finishedWork);
let parent;
let isContainer;
const parentStateNode = parentFiber.stateNode;
//根据父节点的 tag 类型,来判断是否能够作为被插入节点的container,(有可能是组件形式)
switch (parentFiber.tag) {
case HostComponent:
parent = parentStateNode;
isContainer = false;
break;
case HostRoot:
parent = parentStateNode.containerInfo;
isContainer = true;
break;
...
}
// 如果父节点有 ContentReset 的 flags,则重置其文本内容
if (parentFiber.flags & ContentReset) {
resetTextContent(parent);
parentFiber.flags &= ~ContentReset;
}
// 找到 host 的兄弟节点,需要在哪插入
const before = getHostSibling(finishedWork);
if (isContainer) {
insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
} else {
insertOrAppendPlacementNode(finishedWork, before, parent);
}
}
insertOrAppendPlacementNodeIntoContainer
在这个函数中分为两部分
- 如果是原生 DOM 节点,调用
insertInContainerBefore
或appendChildToContainer
来在相应的位置插入 DOM 节点 - 如果不是原生 DOM 节点,会对当前 Fiber 节点的所有子 Fiber 节点调用
insertOrAppendPlacementNodeIntoContainer
对自身进行遍历,直到找到 DOM 节点,然后插入
function insertOrAppendPlacementNodeIntoContainer(
node: Fiber,
before: ?Instance,
parent: Container,
): void {
const {tag} = node;
// 判断当前节点是否为原生的 DOM 节点
const isHost = tag === HostComponent || tag === HostText;
if (isHost) {
const stateNode = node.stateNode;
if (before) {
// 插入
insertInContainerBefore(parent, stateNode, before);
} else {
// 追加
appendChildToContainer(parent, stateNode);
}
} elseif (tag === HostPortal) {
// 不处理
} else {
// 不是原生 DOM 节点,需要遍历插入当前节点的子节点
const child = node.child;
if (child !== null) {
insertOrAppendPlacementNodeIntoContainer(child, before, parent);
let sibling = child.sibling;
while (sibling !== null) {
insertOrAppendPlacementNodeIntoContainer(sibling, before, parent);
sibling = sibling.sibling;
}
}
}
}
insertInContainerBefore 插入节点
当 before
存在时,会进入这个逻辑。也说明当前需要插入节点的前一个节点是明确的了
在这里需要判断当前父节点是否为注释类型的节点
- 如果是注释类型的节点,会在父节点的父节点下插入新的 DOM 节点
- 如果不是,则调用原生 DOM 节点的
insertBefore
方法来直接插入节点
exportfunction insertInContainerBefore(
container: Container,
child: Instance | TextInstance,
beforeChild: Instance | TextInstance | SuspenseInstance,
): void {
if (container.nodeType === COMMENT_NODE) {
(container.parentNode: any).insertBefore(child, beforeChild);
} else {
container.insertBefore(child, beforeChild);
}
}
appendChildToContainer
差不多,不多讲,源码位置 packages/react-dom/src/client/ReactDOMHostConfig.js
Update 更新节点
在前面的代码中我们也能看到,在更新节点时,都会调用 commitWork
函数来处理
下面我们来揭开它的面纱
commitWork
commitWork
函数会对不同类型的更新做出处理,重点关注 HostComponent 和 HostText 类型
整体流程如下
- 首先会判断是否支持 mutation,执行其他的逻辑,这里我们的宿主环境不会进入当前逻辑,跳过这部分
- 接下来会根据 Fiber 节点的 tag 类型,进入不同的条件语句:
对于和 Function Component 相关的类型,例如 simpleMemoComponent
、functionComponent
等类型,会执行 commitHookEffectListUnmount
函数,也就是会调用 useLayoutEffect
或 useInsertionEffect
的销毁函数
具体是会遍历当前的 updateQueue
链表,如果当前 Fiber 节点的 effectTag
等于传入的 tag(HookLayout | Insertion),这个 effectTag
就表示,当前 Fiber 节点包含对 useLayoutEffect
或 useInsertionEffect
的调用,会执行它们的销毁函数
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);
}
}
- 对于 HostComponent 类型的节点,首先会获取到 新旧props以及
updateQueue
,最后调用commitUpdate
来对 DOM 进行更新
case HostComponent: {
// 获取对应的 DOM 节点
const instance: Instance = finishedWork.stateNode;
if (instance != null) {
// 新旧 props
const newProps = finishedWork.memoizedProps;
const oldProps = current !== null ? current.memoizedProps : newProps;
const type = finishedWork.type;
// 获取 updateQueue
const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);
finishedWork.updateQueue = null; // 清空
if (updatePayload !== null) {
// 提交更新
commitUpdate(
instance,
updatePayload,
type,
oldProps,
newProps,
finishedWork,
);
}
}
return;
}
- 对于 HostText 类型的更新,首先获取到真实的文本节点、新旧文本的内容,调用
commitTextUpdate
来更新文本节点的 nodeValue
// FC 相关的 case 调用 commitHookEffectListUnmount
case HostText: {
// ...错误处理
const textInstance: TextInstance = finishedWork.stateNode;
const newText: string = finishedWork.memoizedProps;
const oldText: string =
current !== null ? current.memoizedProps : newText;
// 更新新旧 text
commitTextUpdate(textInstance, oldText, newText);
return;
}
// ... 不关注
}
commitUpdate
在 commitWork
中,会调用 commitUpdate
函数来进行元素的更新,commitUpdate
主要做以下几件事
- 执行
domElement[internalPropsKey] = props
,来更新 props - 然后调用
updateProperties
函数,来更新 DOM 的属性,将diff
的结果应用到真实 DOM 上,首先会对 radio 进行特殊的处理,然后会调用updateDOMProperties
,然后根据 Fiber 的 tag 类型,对 input、textarea、select 等表单类型的节点做处理
exportfunction commitUpdate(
domElement: Instance,
updatePayload: Array<mixed>,
type: string,
oldProps: Props,
newProps: Props,
internalInstanceHandle: Object,
): void {
// domElement[internalPropsKey] = props
updateFiberProps(domElement, newProps);
// 将 diff 结果应用于真实DOM
updateProperties(domElement, updatePayload, type, oldProps, newProps);
}
updateDOMProperties
在 updateDOMProperties
中会遍历 updateQueue
链表,将更新作用到真实 DOM 节点上,根据 propKey 进行不同的更新操作
function updateDOMProperties(
domElement: Element,
updatePayload: Array<any>,
wasCustomComponentTag: boolean,
isCustomComponentTag: boolean,
): void {
// 遍历 updatePayload
for (let i = 0; i < updatePayload.length; i += 2) {
const propKey = updatePayload[i];
const propValue = updatePayload[i + 1];
if (propKey === STYLE) {
// 处理 style
setValueForStyles(domElement, propValue);
} elseif (propKey === DANGEROUSLY_SET_INNER_HTML) {
// 处理 innerHtml
setInnerHTML(domElement, propValue);
} elseif (propKey === CHILDREN) {
// 处理 children
setTextContent(domElement, propValue);
} else {
// 处理其他节点属性
setValueForProperty(domElement, propKey, propValue, isCustomComponentTag);
}
}
}
接下来执行 layout 阶段!
Layout 阶段
在 layout
阶段正式开始之前,也就是在 mutation
阶段之后,会执行 current 树变更的操作,这是一个非常重要的过程
current Fiber 树的切换
在 mutation
阶段和 layout
阶段之间有一句关键的代码
root.current = finishedWork;
在双缓存机制部分中,我们也有写过,当 workInProgress
Fiber 树完成了渲染,就会将 current 指针从 current Fiber
树指向 workInProgress Fiber
树,也就是这行代码所做的工作
为什么要在 mutation 阶段结束后,layout 阶段之前执行呢?
这是因为 componentWillUnmount
这个生命周期钩子函数,会在 mutation
阶段执行,此时可能会操作原来 Fiber 上的内容,为了保证数据的可靠性所以不会修改 current
指针
而在 layout
阶段会执行 componentDidMount
和 componentDidUpdate
生命周期钩子,此时需要获取到的 DOM 是更新后的
流程概览
本部分来讲解 commit
阶段的最后一个子阶段 Layout
阶段的主要工作layout
阶段会执行 commitLayoutEffect
这个方法
commitLayoutEffects(finishedWork, root, lanes);
同样的会分为 begin
和 complete
两部分来执行,核心流程也是在 xxxOnFiber 中执行
在 commitLayoutEffect
函数中,首先会对全局变量 nextEffect
进行赋值
然后会执行 commitLayoutEffects_begin
函数,在这个函数中,会从 nextEffect
开始,向下遍历子树,调用 commitLayoutMountEffects_complete
函数来处理副作用,触发 componentDidMount
、componentDidUpdate
以及各种回调函数等。
commitLayoutEffects
作为入口函数,会对全局变量 nextEffect
进行赋值,调用 commitLayoutEffects_begin
处理副作用,触发生命周期钩子
export function commitLayoutEffects(
finishedWork: Fiber,
root: FiberRoot,
committedLanes: Lanes,
): void {
inProgressLanes = committedLanes;
inProgressRoot = root;
nextEffect = finishedWork;
commitLayoutEffects_begin(finishedWork, root, committedLanes);
inProgressLanes = null;
inProgressRoot = null;
}
commitLayoutEffects_begin
在这个函数中会从 rootFiber 开始,向下遍历。对当前屏幕内的节点调用 commitLayoutMountEffects_complete
函数来处理副作用,触发 componentDidMount
、componentDidUpdate
以及各种回调函数等,跳过未显示的节点。
function commitLayoutEffects_begin(
subtreeRoot: Fiber,
root: FiberRoot,
committedLanes: Lanes,
) {
const isModernRoot = (subtreeRoot.mode & ConcurrentMode) !== NoMode;
while (nextEffect !== null) {
const fiber = nextEffect;
const firstChild = fiber.child;
if (
enableSuspenseLayoutEffectSemantics &&
fiber.tag === OffscreenComponent &&
isModernRoot
) {
// 跟踪当前屏幕外堆栈的状态
const isHidden = fiber.memoizedState !== null;
const newOffscreenSubtreeIsHidden = isHidden || offscreenSubtreeIsHidden;
// 当前节点是否显示
if (newOffscreenSubtreeIsHidden) {
// 遍历 alternate 树进行布局,循环处理兄弟节点和父节点
commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes);
continue;
} else {
...
let child = firstChild;
// 递归调用 commitLayoutEffects_begin
while (child !== null) {
nextEffect = child;
commitLayoutEffects_begin(
child, // New root; bubble back up to here and stop.
root,
committedLanes,
);
child = child.sibling;
}
...
commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes);
continue;
}
}
if ((fiber.subtreeFlags & LayoutMask) !== NoFlags && firstChild !== null) {
ensureCorrectReturnPointer(firstChild, fiber);
nextEffect = firstChild;
} else {
commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes);
}
}
}
commitLayoutMountEffects_complete
在 complete 中,同样会从 nextEffect
开始进行归并。调用 commitLayoutEffectOnFiber
函数,根据不同的组件类型,处理相关的副作用
function commitLayoutMountEffects_complete(
subtreeRoot: Fiber,
root: FiberRoot,
committedLanes: Lanes,
) {
// 循环处理兄弟节点和父节点
while (nextEffect !== null) {
const fiber = nextEffect;
if ((fiber.flags & LayoutMask) !== NoFlags) {
const current = fiber.alternate;
...
commitLayoutEffectOnFiber(root, current, fiber, committedLanes);
...
}
// fiber 树遍历完成
if (fiber === subtreeRoot) {
nextEffect = null;
return;
}
// 遍历 sibling 节点
const sibling = fiber.sibling;
if (sibling !== null) {
ensureCorrectReturnPointer(sibling, fiber.return);
nextEffect = sibling;
return;
}
// 回到 parent 节点,继续遍历
nextEffect = fiber.return;
}
}
commitLayoutEffectOnFiber
在 commitLayoutEffectOnFiber
中会根据 fiber
的 tag
的不同,执行不同的操作
对于 Function component 来说,会调用 commitHookEffectListMount
函数,首先会遍历所有 useLayoutEffect
,去执行它的回调函数
在前面我们知道了 useLayoutEffect
会在 mutation
阶段执行它上一次的销毁函数
在这里我们知道了在 layout
阶段会执行 useLayoutEffect
的回调函数,因此 useLayoutEffect
会先执行所有的销毁函数,再执行回调函数,这两步是同步执行
function commitLayoutEffectOnFiber(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
committedLanes: Lanes,
): void {
switch (finishedWork.tag) {
// FC 相关
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
// 执行 useLayoutEffect 的回调函数
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
return;
}
...
commitAttachRef(finishedWork);
}
对于 ClassComponent 而言
- 如果
current
为null
会调用componentDidMount
这个生命周期函数,因此也可以知道componentDidMount
是在commit layout
阶段同步执行的 - 当
current
不为null
时,会执行componentDidUpdate
生命周期函数,然后会调用commitUpdateQueue
函数,遍历updateQueue
上的effects
,执行effect
副作用 - 如果 setState 有 callback 会放入 updateQueue 中,通过
commitUpdateQueue
来执行 callback 回调函数
current 为 null 时,是首屏渲染
if (current === null) {
...
instance.componentDidMount();
} else {
...
instance.componentDidUpdate(
prevProps,
prevState,
instance.__reactInternalSnapshotBeforeUpdate,
);
}
同样的对于 HostRoot
类型的 Fiber 而言,也会在这里调用 commitUpdateQueue
函数来处理 effects
,接下来看看 commitUpdateQueue
的作用
commitUpdateQueue
commitUpdateQueue
函数会执行 updateQueue
上的 effects
副作用,通过遍历 effects
,如果有 callback
就会执行,否则会重置 updateQueue
上的 effects
为 null
export function commitUpdateQueue<State>(
finishedWork: Fiber,
finishedQueue: UpdateQueue<State>,
instance: any,
): void {
// 遍历 effects 链表,执行 effect callback
const effects = finishedQueue.effects;
finishedQueue.effects = null;
if (effects !== null) {
for (let i = 0; i < effects.length; i++) {
const effect = effects[i];
const callback = effect.callback;
if (callback !== null) {
effect.callback = null;
callCallback(callback, instance);
}
}
}
}
commitAttachRef
在 commitLayoutEffectOnFiber
中做的第二件事就是 commitAttachRef
,获取 DOM 实例,更新 Ref
function commitAttachRef(finishedWork: Fiber) {
const ref = finishedWork.ref;
if (ref !== null) {
const instance = finishedWork.stateNode;
let instanceToUse;
// 获取 DOM 实例
switch (finishedWork.tag) {
case HostComponent:
instanceToUse = getPublicInstance(instance);
break;
default:
instanceToUse = instance;
}
// Moved outside to ensure DCE works with this flag
if (enableScopeAPI && finishedWork.tag === ScopeComponent) {
instanceToUse = instance;
}
if (typeof ref === 'function') {
...
retVal = ref(instanceToUse);
} else {
retVal = ref(instanceToUse);
}
}
}
}
小总结
至此 layout 阶段的工作已经完成了,Layout 做的事情有:
- 对于类组件,会执行
componentDidMount
、componentDidUpdate
生命周期,setState 的 callback - 对于函数组件会执行
useLayoutEffect
、useInsertionEffect
钩子 - 如果有 ref ,会更新 ref
❤️ 谢谢支持
喜欢的话别忘了 分享、点赞、在看 三连哦~。
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。