前言:最近离职准备面试,把之前写的笔记整理一下发出来,本人能力有限,如有错误的地方尽情指正
博客链接:pionpill
对应源码: github.com/facebook/re…
commit 主要阶段可以分为三部分:
- before mutation 阶段: 替换 DOM 前
- mutation 阶段: 替换 DOM
- layout 阶段: 替换 DOM 后
commitBeforeMutationEffects
先回顾一下 BeforeMutationMask
的定义:
BeforeMutationMask = Update | Snapshot;
这暗示了在 before mutation 阶段我们会处理:
- class 组件的
getSnapshotBeforeUpdate
钩子 hostComponent
DOM 节点发生属性变化hostText
发生文本变化,需要更新执行
总的来说这个阶段主要处理 Snapshot
标记。这个标记对应 class 组件的 getSnapshotBeforeUpdate
方法,这是一个类组件的生命周期钩子。由于 Fiber 架构 render 阶段的任务可能中断/重新开始,对应的组件在 render 阶段生命周期钩子可能触发多次,如果我们想要仅执行一次,使用该钩子即可。
update 在三个阶段都会处理,这个阶段其实只有函数组件会执行一部分 update 操作。
我们看一下 commitBeforeMutationEffects
方法(✨约357行):
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
这个方法的核心操作在 commitBeforeMutationEffects_begin
里面(✨约374行):
function commitBeforeMutationEffects_begin() {
while (nextEffect !== null) {
// 遍历 FiberNode 树
const fiber = nextEffect;
const child = fiber.child;
// 如果该fiber的子节点存在 BeforeMutation 阶段相关的flags标记 且 child不为null; 则继续循环,
if ((fiber.subtreeFlags & BeforeMutationMask) !== NoFlags && child !== null) {
child.return = fiber;
nextEffect = child;
} else {
// 子节点没有 BeforeMutation 相关的标记或是叶节点
commitBeforeMutationEffects_complete();
}
}
}
这个过程遍历了一遍 FiberNode 树,每次循环都取出 child
子节点判断是否满足如下条件:
- 该
fiber
的子节点存在BeforeMutation
阶段相关的flags
标记。 child
不为null
。
如果同时满足这两个条件,则将 child
设置为新的 nextEffect
,开启下一个循环。如果不满足这两个条件,则说明当前节点就是需要处理副作用的节点,用 commitBeforeMutationEffects_complete
处理(✨约404行)。
function commitBeforeMutationEffects_complete() {
// 这里从 nextEffect 开始继续遍历 FiberNode 树
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;
}
// 没有右节点,下次循环中处理父节点
nextEffect = fiber.return;
}
}
遍历过程
在看 commitBeforeMutationEffectsOnFiber
之前,我们先理一下 before commit 阶段的遍历过程:
- 从 Root Fiber 开始遍历(
nextEffect
初值设置为FiberNode
)。 - 如果 FiberNode 需要在这一阶段处理,执行如下循环:
- 执行该 FiberNode 在 beforeMutation 要执行的副作用
- 如果有右节点,将
nextEffect
赋给右节点,跳出循环 - 没有右节点,
nextEffect
赋给父节点,继续循环
- 本节点无需更新,则
nextEffect
赋给子节点,继续遍历。
这个过程有两个问题:
- 在外部遍历过程中,每次执行完都会将
nextEffect
赋给子节点,那么右节点需要处理 effect 怎么办?
答: 在 Render 过程中,如果一系列兄弟节点中的某一个FiberNode节点需要更新,那么他们的最左节点会被标记,以确保整个兄弟节点都进行一次遍历判断。 - 在内部遍历过程中,effect 处理完成后会继续处理父节点的 effect,这个过程会一直向上传递吗?
答: 是的,会一直向上传递,考虑这样一种情形: react 允许传函数给子组件,而这个函数有可能导致父组件状态更新!
由于 React 有这两个特性,我们在写的时候就需要注意了。针对第一种情形,有状态的兄弟组件过多的时候可以考虑加一层无状态组件,尽量不要出现 map 多个有状态的组件。针对第二种情形,没事别让子组件改变父组件状态。
commitBeforeMutationEffectsOnFiber
来看看 before commit 阶段对 FiberNode 干了什么吧(✨约426行):
function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
const current = finishedWork.alternate;
const flags = finishedWork.flags;
// 针对不同组件类型进行处理
switch (finishedWork.tag) {
case FunctionComponent: {
if (enableUseEffectEventHook && (flags & Update) !== NoFlags) {
commitUseEffectEventMount(finishedWork);
}
break;
}
case ClassComponent: {
if ((flags & Snapshot) !== NoFlags && 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;
}
case HostRoot: {
if ((flags & Snapshot) !== NoFlags && supportsMutation) {
const root = finishedWork.stateNode;
// 清空根节点内容
clearContainer(root.containerInfo);
}
break;
}
case HostComponent:
case HostHoistable:
case HostSingleton:
case HostText:
case HostPortal:
case IncompleteClassComponent:
// Nothing to do for these component types
break;
default: {
if ((flags & Snapshot) !== NoFlags) {
throw new Error('这类组件不应该有副作用');
}
}
}
}
绝大部分类型的 FiberNode 在这一阶段啥都没做,仅函数组件处理了 Update
标记,类组件和DOM根组件处理了 Snapshot
标记。
我们继续看一下 commitUseEffectEventMount
方法(✨约709行):
function commitUseEffectEventMount(finishedWork: Fiber) {
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;
}
}
}
这个方法用于处理 useEffect
钩子函数中的事件绑定。它会在组件挂载时执行,将事件监听器添加到相应的 DOM 元素上。这样,当事件触发时,React 将能够捕获并处理它们。