一篇带你理解 React 的 Commit 阶段

开发 前端
Commit 分成三个阶段:BeforeMuation、Muation 以及 Layout 阶段。

大家好,我是前端西瓜哥。今天我们来详细讲解一下 React 的 commit 阶段的逻辑。

React 版本为 18.2.0

commit 分三个阶段:

  1. BeforeMutation。
  2. Mutation:在这里更新 DOM。
  3. Layout。

commitRootImpl 中的三个函数的调用分别对应这个三个阶段:

function commitRootImpl(){
// BeforeMutation 阶段
commitBeforeMutationEffects(root, finishedWork);
// Mutation 阶段
commitMutationEffects(root, finishedWork, lanes);
// Layout 阶段
commitLayoutEffects(finishedWork, root, lanes);
}

一些标记

在 reconcil (调和)阶段,给 fiber 打了很多的 flags(标记),commit 阶段是会读取这些 flags 进行不同的操作的。

flags 是通过二进制掩码的方式来保存的,掩码优点是节省内存,缺点是可读性很差。

使用或位运算,可以将多个 flag 组合成一个组。

我这三个阶段 用到的组合掩码 为:

export const BeforeMutationMask =
Update |
Snapshot;

export const MutationMask =
Placement |
Update |
ChildDeletion |
ContentReset |
Ref |
Hydrating |
Visibility;

export const LayoutMask = Update | Callback | Ref | Visibility;

BeforeMutation 阶段

BeforeMutation 阶段。

commitRootImpl 首先会 调用 commitBeforeMutationEffects 方法。

commitBeforeMutationEffects 的核心实现:

function commitBeforeMutationEffects(root, firstChild) {
nextEffect = firstChild;
commitBeforeMutationEffects_begin();
}

主要是调用这个 commitBeforeMutationEffects_begin 方法。

begin

begin 干了啥?

进行深度优先遍历,找到最后一个带有 BeforeMutation 标识的 fiber。这是因为 useEffect 的调用逻辑是从子到父,要找最后一个 fiber 作为起点。

commitBeforeMutationEffects_begin 的核心实现:

function commitBeforeMutationEffects_begin() {
while (nextEffect !== null) {
const fiber = nextEffect;
// 取出子 fiber
const child = fiber.child;
if (
(fiber.subtreeFlags & BeforeMutationMask) !== NoFlags &&
child !== null
) {
// 说明子 fiber 树中存在 BeforeMutationMask 标识的 fiber
// 那就继续遍历往下找
child.return = fiber;
nextEffect = child;
} else {
// 找不到了,说明到底了,执行 complete 逻辑。
commitBeforeMutationEffects_complete();
}
}
}

subtreeFlags 是当前 fiber 的子树的标识汇总,目的是防止无意义的完整深度遍历,能够更早地结束遍历。如果直接用 flags,是要遍历到叶子节点才能知道到底谁是要找的最有一个节点。

找到后,调用 complete 。

complete

commitBeforeMutationEffects_complete 实现为:

function commitBeforeMutationEffects_complete() {
while (nextEffect !== null) {
const fiber = nextEffect;
try {
// BeforeMutation 阶段真正做的事情
commitBeforeMutationEffectsOnFiber(fiber);
} catch (error) {
captureCommitPhaseError(fiber, fiber.return, error);
}

const sibling = fiber.sibling;
if (sibling !== null) { // 没有下一个兄弟节点
sibling.return = fiber.return;
nextEffect = sibling;
return;
// 结束后会回到 begin 中的循环中
// 继续往下找最后一个 带有 BeforeMutation 标识的 fiber
}

// 从上往下,处理
nextEffect = fiber.return;
}
}

前面很多逻辑都是遍历的逻辑,真正的核心操作在 commitBeforeMutationEffectsOnFiber 方法。

做了什么?

对标记了 Snapshot 的组件进行处理,通常是类组件,会 调用类组件实例 instance 的 getSnapshotBeforeUpdate 方法,生成快照对象,然后再放到 instance.__reactInternalSnapshotBeforeUpdate 下,作为之后的 componentDidUpdate 钩子函数的第三个参数。

其他类型的组件基本啥都不做。

function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
const current = finishedWork.alternate;
const flags = finishedWork.flags;

// flags 存在 Snapshot
if ((flags & Snapshot) !== NoFlags) {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
break;
}
// 类组件
case ClassComponent: {
if (current !== null) {
const prevProps = current.memoizedProps;
const prevState = current.memoizedState;
const instance = finishedWork.stateNode;
// 调用类组件实例的 getSnapshotBeforeUpdate 生成快照对象
const snapshot = instance.getSnapshotBeforeUpdate(
finishedWork.elementType === finishedWork.type
? prevProps
: resolveDefaultProps(finishedWork.type, prevProps),
prevState,
);

instance.__reactInternalSnapshotBeforeUpdate = snapshot;
}
break;
}
case HostRoot: {
if (supportsMutation) {
const root = finishedWork.stateNode;
clearContainer(root.containerInfo);
}
break;
}
case HostComponent:
case HostText:
case HostPortal:
case IncompleteClassComponent:
// 啥也不做
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.',
);
}
}
}
}

Mutation 阶段

mutation 阶段是最重要的阶段,在这个阶段,React 真正地更新了文档 DOM 树。

入口函数是 commitMutationEffects,但它只是 commitMutationEffectsOnFiber 的封装。

function commitMutationEffects(root, finishedWork, committedLanes) {
inProgressLanes = committedLanes;
inProgressRoot = root;
commitMutationEffectsOnFiber(finishedWork, root, committedLanes);
inProgressLanes = null;
inProgressRoot = null;
}

每个 fiber 都要传入 commitMutationEffectsOnFiber,执行 mutation 主逻辑。

从这调用栈可知 commitMutationEffectsOnFiber 递归调用了多次,形成了很长的调用栈。

图片

commitMutationEffectsOnFiber 的核心实现为:

function commitMutationEffectsOnFiber(finishedWork, root, lanes) {
const current = finishedWork.alternate;
const flags = finishedWork.flags;

switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
// 做了一些事情
commitReconciliationEffects(finishedWork);
}
// 类组件
case ClassComponent: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);

if (flags & Ref) {
if (current !== null) {
safelyDetachRef(current, current.return);
}
}
return;
}
}

对不同类型的 fiber 会进行不同的处理,但有一些公共逻辑会执行的,那就是:

// Deletion 深度遍历执行删除操作
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
// Placement 插入逻辑
commitReconciliationEffects(finishedWork);

删除逻辑

首先是调用 recursivelyTraverseMutationEffects 方法,这个方法会执行删除逻辑。

该方法会读取 fiber 的 deletions 数组,对这些要删除的 fiber 进行操作。

function recursivelyTraverseMutationEffects(root, parentFiber, lanes) {
// Deletions effects can be scheduled on any fiber type. They need to happen
// before the children effects hae fired.
const deletions = parentFiber.deletions;
if (deletions !== null) {
for (let i = 0; i < deletions.length; i++) {
const childToDelete = deletions[i];
try {
// 执行 fiber 的删除逻辑
commitDeletionEffects(root, parentFiber, childToDelete);
} catch (error) {
captureCommitPhaseError(childToDelete, parentFiber, error);
}
}
}
// 【下面的可不看】其实就是对子节点遍历,也执行 mutation 主逻辑。
if (parentFiber.subtreeFlags & MutationMask) {
let child = parentFiber.child;
while (child !== null) {
// 又调用 mutation 逻辑的入口函数
commitMutationEffectsOnFiber(child, root, lanes);
child = child.sibling;
}
}
}

对于要删除的 fiber,我们这里讨论原生组件、类组件、函数组件这 3 种组件类型 fiber 的删除逻辑。

【1】原生组件

对于原生组件类型(div、span 这些):

  1. 首先将 绑定的 ref 置为 null。
  2. 先递归,对它的子 fiber 调用删除逻辑。
  3. 然后 从 DOM 树中删除对应 DOM。
function commitDeletionEffectsOnFiber(finishedRoot, nearestMountedAncestor, deletedFiber) {
// deletedFiber 表示那个要被删除的 fiber

switch (deletedFiber.tag) {
/********* 原生组件 *********/
case HostComponent: {
if (!offscreenSubtreeWasHidden) {
// ref 设置回 null
safelyDetachRef(deletedFiber, nearestMountedAncestor);
}
// 后面的 HostText 会接着执行,switch 就是这个逻辑
}
case HostText: {
const prevHostParent = hostParent;
const prevHostParentIsContainer = hostParentIsContainer;
hostParent = null;

// 往下遍历子节点,执行删除
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber,
);

hostParent = prevHostParent;
hostParentIsContainer = prevHostParentIsContainer;

// 删除真正的 DOM,调用了原生的 removeChild 方法
if (hostParent !== null) {
if (hostParentIsContainer) {
removeChildFromContainer(
((hostParent: any): Container),
(deletedFiber.stateNode: Instance | TextInstance),
);
} else {
removeChild(
((hostParent: any): Instance),
(deletedFiber.stateNode: Instance | TextInstance),
);
}
}
return;
}
// 其他组件类型
}

【2】类组件

对于类组件:

  1. 先重置 ref。
  2. 然后 调用 componentWillUnmount 方法。
  3. 最后递归,对它的子 fiber 调用删除逻辑。
function commitDeletionEffectsOnFiber(finishedRoot, nearestMountedAncestor, deletedFiber) {
// deletedFiber 表示那个要被删除的 fiber

switch (deletedFiber.tag) {
// ...

/********* 类组件 *********/
case ClassComponent: {
if (!offscreenSubtreeWasHidden) {
// 移除 ref
safelyDetachRef(deletedFiber, nearestMountedAncestor);
const instance = deletedFiber.stateNode;
if (typeof instance.componentWillUnmount === 'function') {
// 调用类组件实例的 componentWillUnmount 方法
safelyCallComponentWillUnmount(
deletedFiber,
nearestMountedAncestor,
instance,
);
}
}
// 遍历子节点执行删除逻辑
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber,
);
return;
}
// ...
}
}

【3】函数组件

对于函数组件:

  1. 遍历它的 updateQueue 队列,并通过 effect 的 tag 来识别类型来决定是否调用 destory 方法。对 useInsertionEffect 和 useLayoutEffect,调用它们的 destory 方法。destroy 就是执行 useInsertionEffect / useLayoutEffect 的回调函数所返回的函数。useEffect 则跳过,不调用 destory 方法。
  2. 最后递归,对它的子 fiber 调用删除逻辑。
function commitDeletionEffectsOnFiber(finishedRoot, nearestMountedAncestor, deletedFiber) {
// deletedFiber 表示那个要被删除的 fiber

switch (deletedFiber.tag) {
// ...

/********* 函数组件 *********/
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
if (!offscreenSubtreeWasHidden) {
const updateQueue = deletedFiber.updateQueue;
if (updateQueue !== null) {
// 读取 updateQueue 队列,队列用链表的方式保存
const lastEffect = updateQueue.lastEffect;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;

let effect = firstEffect;
do {
const {destroy, tag} = effect;
if (destroy !== undefined) {
// 处理 useInsertionEffect 产生的副作用
// 执行 useInsertionEffect 回调函数返回的函数,即 destroy
if ((tag & HookInsertion) !== NoHookEffect) {
// safelyCallDestroy 只是加了 try-catch 去调用 destroy
safelyCallDestroy(
deletedFiber,
nearestMountedAncestor,
destroy,
);

// useLayoutEffect 同理
} else if ((tag & HookLayout) !== NoHookEffect) {
safelyCallDestroy(
deletedFiber,
nearestMountedAncestor,
destroy,
);
}
}
// 找下一个 effect
effect = effect.next;
} while (effect !== firstEffect);
}
}
}

// 向下递归
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber,
);
return;
}

default: {
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber,
);
return;
}
}
}

插入逻辑

完成删除逻辑后,接着就是调用 commitReconciliationEffects,这个方法负责往真实 DOM 树中插入 DOM 节点。

commitReconciliationEffects 核心内容:

function commitReconciliationEffects(finishedWork) {
const flags = finishedWork.flags;

if (flags & Placement) {
try {
// 执行 Placement 插入逻辑
commitPlacement(finishedWork);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
// 移除 Placement 标志
finishedWork.flags &= ~Placement;
}

if (flags & Hydrating) {
finishedWork.flags &= ~Hydrating;
}
}

如果 finishedWork 有 Placement 标识,则调用 commitPlacement 方法。

commitPlacement 的逻辑为:

  1. 如果 finishedWork 还有 ContentReset 标识,先清空标签体,通过parent.textContent = '' 的方式。
  2. 接着是执行插入逻辑。会尝试找下一个兄弟节点,存在会原生的 insertBefore 方法插入,不存在则使用 appendChild 方法。

commitPlacement 实现如下。

function commitPlacement(finishedWork) {
// 获取父 fiber
const parentFiber = getHostParentFiber(finishedWork);

switch (parentFiber.tag) {
case HostComponent: {
const parent = parentFiber.stateNode;
// 父 fiber 是否有 ContentReset(内容重置)标记
if (parentFiber.flags & ContentReset) {
// 其实就是 parent.textContent = '';
resetTextContent(parent); //
// 移除 ContentReset 标志
parentFiber.flags &= ~ContentReset;
}

// 找它的下一个兄弟 DOM 节点,后面用 insertBefore 方法
// 如果没有,就调用原生的 appendChild 方法
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;
}
// eslint-disable-next-line-no-fallthrough
default:
throw new Error(
'Invalid host parent fiber. This error is likely caused by a bug ' +
'in React. Please file an issue.',
);
}
}

Placement 只会在原生组件和 fiber 根节点上标记,没有函数组件和类组件什么事。

更新逻辑

对于可复用的原生组件,会 调用 commitUpdate 进行更新。

commitUpdate 的代码:

function commitUpdate(domElement, updatePayload, type, oldProps, newProps) {
// 对比更新,需要处理 onXx、className 这些特殊的 props
updateProperties(domElement, updatePayload, type, oldProps, newProps);

// 更新 DOM 元素的 "__reactProps$ + randomKey" 为这个新的 props
updateFiberProps(domElement, newProps);
}

​类组件不会进行更新操作。

对于函数组件,会依次调用:

  • useInsertionEffect 的回调函数函数返回的销毁函数(保存在 effect.destroy 中)。
  • useInsertionEffect 的回调函数(保存在 effect.create 中),调用完后将返回结果赋值个 effect.destroy,下一次更新再调用。
  • useLayoutEffect 的回调函数函数返回的销毁函数。

需要注意,函数组件初次挂载,flags 也会标记为 Update,走更新逻辑。这也是为什么 useEffect 在函数组件挂载时也会执行,和类组件的 componentDidUpate 不同。

// 找出 useInsertionEffect 的 destroy 方法去调用
// 需要注意 destroy 可能为 undefined(函数组件初次挂载的情况下)
commitHookEffectListUnmount(HookInsertion | HookHasEffect, finishedWork, finishedWork.return);

// 执行 useInsertionEffect 的回调函数,并将返回值保存到 effect.destory 里。
commitHookEffectListMount(HookInsertion | HookHasEffect, finishedWork);

// useLayoutEffect 对应的 destroy 方法
// 同样可能不存在
commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork, finishedWork.return);

Layout

最后是 Layout 阶段。

commitLayoutEffects 的实现:

function commitLayoutEffects(finishedWork, root, committedLanes) {
inProgressLanes = committedLanes;
inProgressRoot = root;
nextEffect = finishedWork;

// 又是 begin,和 BeforeMutation 阶段类似的递归逻辑
commitLayoutEffects_begin(finishedWork, root, committedLanes);

inProgressLanes = null;
inProgressRoot = null;
}

和 BeforeMutation 阶段一样,先深度优先递归,找最后一个有 LayoutMask 标记的 fiber。

然后从下往上调用 complete 逻辑,确保逻辑是从底部到顶部,即先子后父。

function commitLayoutEffects_begin(subtreeRoot, root, committedLanes) {
// Suspense layout effects semantics don't change for legacy roots.
const isModernRoot = (subtreeRoot.mode & ConcurrentMode) !== NoMode;

while (nextEffect !== null) {
const fiber = nextEffect;
const firstChild = fiber.child;

if (fiber.tag === OffscreenComponent) {
// 离屏组件的逻辑,不讲
continue;
}

// 找最后一个有 LayoutMask 标记的 fiber
if ((fiber.subtreeFlags & LayoutMask) !== NoFlags && firstChild !== null) {
firstChild.return = fiber;
nextEffect = firstChild;
} else {
// 到底了,就执行 complete
commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes);
}
}
}

complete 代码:

function commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes) {
while (nextEffect !== null) {
const fiber = nextEffect;
if ((fiber.flags & LayoutMask) !== NoFlags) {
const current = fiber.alternate;

try {
// 调用 commitLayoutEffectOnFiber
commitLayoutEffectOnFiber(root, current, fiber, committedLanes);
} catch (error) {
captureCommitPhaseError(fiber, fiber.return, error);
}

}

if (fiber === subtreeRoot) {
nextEffect = null;
return;
}

const sibling = fiber.sibling;
if (sibling !== null) {
sibling.return = fiber.return;
nextEffect = sibling;
return;
}

nextEffect = fiber.return;
}
}

核心工作在这个 commitLayoutEffectOnFiber 方法。它会根据组件类型不同执行不同逻辑。

对于函数组件,会调用 useLayoutEffect 的回调函数(effect.create)。

对于类组件:

  1. 如果是挂载(通过 fiber.alternate 是否为 null 判断),调用 instance.componentDidMount 方法。如果是更新,提取 preProps 等参数 传入到 componentDidUpdate 里调用;
  2. 取出 updateQueue 里的 effect,依次调用 effect.callback 函数。这个 callback 其实就是 setState 方法的第二个参数。

处理完后,接下来就会 更新 ref :

if (finishedWork.flags & Ref) {
commitAttachRef(finishedWork);
}

操作很简单,对于原生组件,就是给 fiber.ref.current 赋值为 fiber.stateNode。

useEffect

现在还差 useEffect 没调用了。

useEffect 不在同步的 commit 阶段中执行。它是异步的,被 scheduler 异步调度执行。

function commitRootImpl(){
// 异步调度
scheduleCallback(NormalSchedulerPriority, () => {
// 执行 useEffect
flushPassiveEffects();
return null;
});

// BeforeMutation 阶段
commitBeforeMutationEffects(root, finishedWork);
// Mutation 阶段
commitMutationEffects(root, finishedWork, lanes);
// Layout 阶段
commitLayoutEffects(finishedWork, root, lanes);
}

先执行所有 useEffect 的 destroy 方法,然后才执行所有 useEffect 的 create 方法。并保持顺序是先子后父。

function flushPassiveEffectsImpl() {
// ...
// useEffect 的 destroy
commitPassiveUnmountEffects(root.current);
// useEffect 的 create
commitPassiveMountEffects(root, root.current, lanes, transitions);
// ...
}

流程图

画个流程图:

图片

create 表示传给 useEffect 的回调函数,destroy 为调用该回调函数返回的销毁函数。

结尾

总结一下。

commit 分成三个阶段:BeforeMuation、Muation 以及 Layout 阶段。

  1. BeforeMuation,没做太多事,主要是类组件实例调用 getSnapshotBeforeUpdate 生成快照对象保存起来;
  2. Muation,更新 DOM 的阶段,做了删除、插入、更新操作。(1)删除逻辑:重置 ref 为 null,根据 fiber.deletions 删除 DOM 节点,调用类组件的 componentWillUnmount,调用 useInsertionEffect 和 useLayoutEffect 的 destory 方法(2)插入逻辑:将标记了Place 的节点进行真实 DOM 的插入(3)对比 props 更新 DOM 节点,调用 useInsertionEffect 的 destroy 、useInsertionEffect 的 create 和 useLayoutEffect 的 destroy;
  3. Layout,调用类组件的 componentDidMount、componentDidUpdate、setState 的回调函数;调用函数组件 useLayoutEffect 的 create;最后更新 ref。

最后是 commit 阶段外的 useEffect,它被 Scheduler 异步调度执行,先执行完整棵树的 destroy,再执行完整棵树的 create。

责任编辑:姜华 来源: 前端西瓜哥
相关推荐

2022-03-10 08:31:51

REST接口规范设计Restful架构

2020-11-27 08:02:41

Promise

2022-11-10 16:55:41

ReactFiber

2021-05-20 06:57:16

RabbitMQ开源消息

2023-04-20 08:00:00

ES搜索引擎MySQL

2020-12-29 05:35:43

FlinkSQL排序

2021-05-17 05:51:31

KubeBuilderOperator测试

2021-05-12 06:18:19

KubeBuilderOperatork8s

2022-02-24 07:56:42

开发Viteesbuild

2021-05-18 05:40:27

kubebuilderwebhook进阶

2022-02-18 08:54:21

docker操作系统Linux

2022-05-05 07:40:07

maskCSS

2021-08-11 07:02:21

npm包管理器工具

2021-06-16 08:28:25

unary 方法函数技术

2023-02-28 23:04:15

2021-11-24 08:51:32

Node.js监听函数

2021-08-02 06:34:55

Redis删除策略开源

2021-11-16 14:09:58

Containerd Dockerk8s

2021-11-08 08:42:44

CentOS Supervisor运维

2023-04-21 08:11:54

KubernetesPod
点赞
收藏

51CTO技术栈公众号