点击进入 React 源码调试仓库。
当 render 阶段实现后,意味着在内存中构建的 workInProgress 树所有更新工作曾经实现,这包含树中 fiber 节点的更新、diff、effectTag 的标记、effectList 的收集。此时 workInProgress 树的残缺状态如下:
和 current 树相比,它们的构造上诚然存在区别,变动的 fiber 节点也存在于 workInProgress 树,但要将这些节点利用到 DOM 上却不会循环整棵树,而是通过循环 effectList 这个链表来实现,这样保障了只针对有变动的节点做工作。
所以循环 effectList 链表去将有更新的 fiber 节点利用到页面上是 commit 阶段的次要工作。
commit 阶段的入口是 commitRoot
函数,它会告知 scheduler 以立刻执行的优先级去调度 commit 阶段的工作。
function commitRoot(root) {const renderPriorityLevel = getCurrentPriorityLevel();
runWithPriority(
ImmediateSchedulerPriority,
commitRootImpl.bind(null, root, renderPriorityLevel),
);
return null;
}
scheduler 去调度的是commitRootImpl
,它是 commit 阶段的外围实现,整个 commit 阶段被划分成三个局部。
commit 流程概览
commit 阶段次要是针对 root 上收集的 effectList 进行解决。在真正的工作开始之前,有一个筹备阶段,次要是变量的赋值,以及将 root 的 effect 退出到 effectList 中。随后开始针对 effectList 分三个阶段进行工作:
- before mutation:读取组件变更前的状态,针对类组件,调用 getSnapshotBeforeUpdate,让咱们能够在 DOM 变更前获取组件实例的信息;针对函数组件,异步调度 useEffect。
- mutation:针对 HostComponent,进行相应的 DOM 操作;针对类组件,调用 componentWillUnmount;针对函数组件,执行 useLayoutEffect 的销毁函数。
- layout:在 DOM 操作实现后,读取组件的状态,针对类组件,调用生命周期 componentDidMount 和 componentDidUpdate,调用 setState 的回调;针对函数组件填充 useEffect 的 effect 执行数组,并调度 useEffect
before mutation 和 layout 针对函数组件的 useEffect 调度是互斥的,只能发动一次调度
workInProgress 树切换到 current 树的机会是在 mutation 完结后,layout 开始前。这样做的起因是在 mutation 阶段调用类组件的 componentWillUnmount 的时候,
还能够获取到卸载前的组件信息;在 layout 阶段调用 componentDidMount/Update 时,获取的组件信息更新后的。
function commitRootImpl(root, renderPriorityLevel) {
// 进入 commit 阶段,先执行一次之前未执行的 useEffect
do {flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);
// 筹备阶段 -----------------------------------------------
const finishedWork = root.finishedWork;
const lanes = root.finishedLanes;
if (finishedWork === null) {return null;}
root.finishedWork = null;
root.finishedLanes = NoLanes;
root.callbackNode = null;
root.callbackId = NoLanes;
// effectList 的整顿,将 root 上的 effect 连到 effectList 的开端
let firstEffect;
if (finishedWork.effectTag > PerformedWork) {if (finishedWork.lastEffect !== null) {
finishedWork.lastEffect.nextEffect = finishedWork;
firstEffect = finishedWork.firstEffect;
} else {firstEffect = finishedWork;}
} else {
// There is no effect on the root.
firstEffect = finishedWork.firstEffect;
}
// 筹备阶段完结,开始解决 effectList
if (firstEffect !== null) {
...
// before mutation 阶段 --------------------------------
nextEffect = firstEffect;
do {...} while (nextEffect !== null);
...
// mutation 阶段 ---------------------------------------
nextEffect = firstEffect;
do {...} while (nextEffect !== null);
// 将 wprkInProgress 树切换为 current 树
root.current = finishedWork;
// layout 阶段 -----------------------------------------
nextEffect = firstEffect;
do {...} while (nextEffect !== null);
nextEffect = null;
// 告诉浏览器去绘制
requestPaint();} else {
// 没有 effectList,间接将 wprkInProgress 树切换为 current 树
root.current = finishedWork;
}
const rootDidHavePassiveEffects = rootDoesHavePassiveEffects;
// 获取尚未解决的优先级,比方之前被跳过的工作的优先级
remainingLanes = root.pendingLanes;
// 将被跳过的优先级放到 root 上的 pendingLanes(待处理的优先级)上
markRootFinished(root, remainingLanes);
/*
* 每次 commit 阶段实现后,再执行一遍 ensureRootIsScheduled,确保是否还有工作须要被调度。* 例如,高优先级插队的更新实现后,commit 实现后,还会再执行一遍,保障之前跳过的低优先级工作
* 从新调度
*
* */
ensureRootIsScheduled(root, now());
...
return null;
}
上面的局部,是对这三个阶段别离进行的具体解说。
before Mutation
beforeMutation 阶段的入口函数是commitBeforeMutationEffects
nextEffect = firstEffect;
do {
try {commitBeforeMutationEffects();
} catch (error) {...}
} while (nextEffect !== null);
它的作用次要是调用类组件的getSnapshotBeforeUpdate
,针对函数组件,异步调度 useEffect。
function commitBeforeMutationEffects() {while (nextEffect !== null) {
const current = nextEffect.alternate;
...
const flags = nextEffect.flags;
if ((flags & Snapshot) !== NoFlags) {
// 通过 commitBeforeMutationEffectOnFiber 调用 getSnapshotBeforeUpdate
commitBeforeMutationEffectOnFiber(current, nextEffect);
}
if ((flags & Passive) !== NoFlags) {
// 异步调度 useEffect
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalSchedulerPriority, () => {flushPassiveEffects();
return null;
});
}
}
nextEffect = nextEffect.nextEffect;
}
}
commitBeforeMutationEffectOnFiber 代码如下
function commitBeforeMutationLifeCycles(
current: Fiber | null,
finishedWork: Fiber,
): void {switch (finishedWork.tag) {
...
case ClassComponent: {if (finishedWork.flags & Snapshot) {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,
);
// 将返回值存储在外部属性上,不便 componentDidUpdate 获取
instance.__reactInternalSnapshotBeforeUpdate = snapshot;
}
}
return;
}
...
}
}
mutation
mutation 阶段会真正操作 DOM 节点,波及到的操作有增、删、改。入口函数是commitMutationEffects
nextEffect = firstEffect;
do {
try {commitMutationEffects(root, renderPriorityLevel);
} catch (error) {
...
nextEffect = nextEffect.nextEffect;
}
} while (nextEffect !== null);
因为过程较为简单,所以我写了三篇文章来阐明这三种 DOM 操作,如果想要理解细节,能够看一下。文章写于 17 还未正式公布的时候,所以外面的源码版本取自 17.0.0-alpha0。
React 和 DOM 的那些事 - 节点新增算法
React 和 DOM 的那些事 - 节点删除算法
React 和 DOM 的那些事 - 节点更新
layout 阶段
layout 阶段的入口函数是commitLayoutEffects
。
nextEffect = firstEffect;
do {
try {commitLayoutEffects(root, lanes);
} catch (error) {
...
nextEffect = nextEffect.nextEffect;
}
} while (nextEffect !== null);
咱们只关注 classComponent 和 functionComponent。针对前者,调用生命周期 componentDidMount 和 componentDidUpdate,调用 setState 的回调;针对后者,填充 useEffect 的 effect 执行数组,并调度 useEffect(具体的原理在我的这篇文章:梳理 useEffect 和 useLayoutEffect 的原理与区别中有解说)。
function commitLifeCycles(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
committedLanes: Lanes,
): void {switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block: {
// 执行 useLayoutEffect 的创立
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
// 填充 useEffect 的 effect 执行数组
schedulePassiveEffects(finishedWork);
return;
}
case ClassComponent: {
const instance = finishedWork.stateNode;
if (finishedWork.flags & Update) {if (current === null) {
// 如果是初始挂载阶段,调用 componentDidMount
instance.componentDidMount();} else {
// 如果是更新阶段,调用 componentDidUpdate
const prevProps =
finishedWork.elementType === finishedWork.type
? current.memoizedProps
: resolveDefaultProps(finishedWork.type, current.memoizedProps);
const prevState = current.memoizedState;
instance.componentDidUpdate(
prevProps,
prevState,
// 将 getSnapshotBeforeUpdate 的后果传入
instance.__reactInternalSnapshotBeforeUpdate,
);
}
}
// 调用 setState 的回调
const updateQueue: UpdateQueue<
*,
> | null = (finishedWork.updateQueue: any);
if (updateQueue !== null) {commitUpdateQueue(finishedWork, updateQueue, instance);
}
return;
}
...
}
}
总结
commit 阶段将 effectList 的解决分成三个阶段保障了不同生命周期函数的适时调用。绝对于同步执行的 useEffectLayout,useEffect 的异步调度提供了一种不阻塞页面渲染的副作用操作入口。另外,标记 root 上还未解决的优先级和调用 ensureRootIsScheduled 使得被跳过的低优先级工作得以再次被调度。commit 阶段的实现,也就意味着本次更新曾经完结。
欢送扫码关注公众号,发现更多技术文章