前言
建议没有看过前面系列文章从前面看起,这是本系列的第三篇文章,这篇文章文章主要讲commit阶段,这个阶段的主要工作是处理副作用。所谓副作用就是不确定操作,比如:插入,替换,删除DOM,还有例如useEffect()hook的回调函数都会被作为副作用
commitWork
流程概述
commitRoot方法是commit阶段工作的起点。fiberRootNode会作为传参。
commitRoot(root);
在rootFiber.firstEffect上保存了一条需要执行副作用的Fiber节点的单向链表effectList,这些Fiber节点的updateQueue中保存了变化的props。
这些副作用对应的DOM操作在commit阶段执行。
除此之外,一些生命周期钩子(比如componentDidXXX)、hook(比如useEffect)需要在commit阶段执行。·
commit阶段的主要工作(即Renderer的工作流程)分为三部分:
- before mutation阶段(执行
DOM操作前) - mutation阶段(执行
DOM操作) - layout阶段(执行
DOM操作后)
berfore mutation阶段
before muation阶段主要是遍历effectList并调用commitBeforeMutationEffects函数处理。
// 保存之前的优先级,以同步优先级执行,执行完毕后恢复之前优先级
const previousLanePriority = getCurrentUpdateLanePriority();
setCurrentUpdateLanePriority(SyncLanePriority);
// 将当前上下文标记为CommitContext,作为commit阶段的标志
const prevExecutionContext = executionContext;
executionContext |= CommitContext;
// 处理focus状态
focusedInstanceHandle = prepareForCommit(root.containerInfo);
shouldFireAfterActiveInstanceBlur = false;
// beforeMutation阶段的主函数
commitBeforeMutationEffects(finishedWork);
focusedInstanceHandle = null;
commitBeforeMutationEffects
function commitBeforeMutationEffects() {
while (nextEffect !== null) {
const current = nextEffect.alternate;
if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) {
// ...focus blur相关
}
const effectTag = nextEffect.effectTag;
// 调用getSnapshotBeforeUpdate
if ((effectTag & Snapshot) !== NoEffect) {
commitBeforeMutationEffectOnFiber(current, nextEffect);
}
// 调度useEffect
if ((effectTag & Passive) !== NoEffect) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}
}
nextEffect = nextEffect.nextEffect;
}
}
整体可以分为三部分:
- 处理
DOM节点渲染/删除后的autoFocus、blur逻辑。 - 调用
getSnapshotBeforeUpdate生命周期钩子。 - 调度
useEffect(异步)。
调度useEffect
// 调度useEffect
if ((effectTag & Passive) !== NoEffect) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalSchedulerPriority, () => {
// 触发useEffect
flushPassiveEffects();
return null;
});
}
}
在此处,被异步调度的回调函数就是触发useEffect的方法flushPassiveEffects。
我们接下来讨论useEffect如何被异步调度,以及为什么要异步(而不是同步)调度
在flushPassiveEffects方法内部会从全局变量rootWithPendingPassiveEffects获取effectList。
在上一篇文章中我们讲到,effectList中保存了需要执行副作用的Fiber节点。其中副作用包括
- 插入
DOM节点(Placement) - 更新
DOM节点(Update) - 删除
DOM节点(Deletion)
除此外,当一个FunctionComponent含有useEffect或useLayoutEffect,他对应的Fiber节点也会被赋值effectTag。
在flushPassiveEffects方法内部会遍历rootWithPendingPassiveEffects(即effectList)执行effect回调函数。
可见,useEffect异步执行的原因主要是防止同步执行时阻塞浏览器渲染。
mutation 阶段
类似before mutation阶段,mutation阶段也是遍历effectList,执行函数。这里执行的是commitMutationEffects。
nextEffect = firstEffect;
do {
try {
commitMutationEffects(root, renderPriorityLevel);
} catch (error) {
invariant(nextEffect !== null, 'Should be working on an effect.');
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
} while (nextEffect !== null);
commitMutationEffects
function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
// 遍历effectList
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
// 根据 ContentReset effectTag重置文字节点
if (effectTag & ContentReset) {
commitResetTextContent(nextEffect);
}
// 更新ref
if (effectTag & Ref) {
const current = nextEffect.alternate;
if (current !== null) {
commitDetachRef(current);
}
}
// 根据 effectTag 分别处理
const primaryEffectTag =
effectTag & (Placement | Update | Deletion | Hydrating);
switch (primaryEffectTag) {
// 插入DOM
case Placement: {
commitPlacement(nextEffect);
nextEffect.effectTag &= ~Placement;
break;
}
// 插入DOM 并 更新DOM
case PlacementAndUpdate: {
// 插入
commitPlacement(nextEffect);
nextEffect.effectTag &= ~Placement;
// 更新
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
// SSR
case Hydrating: {
nextEffect.effectTag &= ~Hydrating;
break;
}
// SSR
case HydratingAndUpdate: {
nextEffect.effectTag &= ~Hydrating;
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
// 更新DOM
case Update: {
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
// 删除DOM
case Deletion: {
commitDeletion(root, nextEffect, renderPriorityLevel);
break;
}
}
nextEffect = nextEffect.nextEffect;
}
}
commitMutationEffects会遍历effectList,对每个Fiber节点执行如下三个操作:
- 根据
ContentReset effectTag重置文字节点 - 更新
ref - 根据
effectTag分别处理,其中effectTag包括(Placement|Update|Deletion|Hydrating)
我们关注步骤三中的Placement | Update | Deletion。Hydrating作为服务端渲染相关,我们先不关注
Placement effect
当Fiber节点含有Placement effectTag,意味着该Fiber节点对应的DOM节点需要插入到页面中。
调用的方法为commitPlacement。
commitPlacement方法所做的工作主要为三步
- 获取父级
DOM节点。其中finishedWork为传入的Fiber节点。
const parentFiber = getHostParentFiber(finishedWork);
// 父级DOM节点
const parentStateNode = parentFiber.stateNode;
- 获取
Fiber节点的DOM兄弟节点
const before = getHostSibling(finishedWork);
- 根据
DOM兄弟节点是否存在决定调用parentNode.insertBefore或parentNode.appendChild执行DOM插入操作。
// parentStateNode是否是rootFiber
if (isContainer) {
insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
} else {
insertOrAppendPlacementNode(finishedWork, before, parent);
}
Update effect
当Fiber节点含有Update effectTag,意味着该Fiber节点需要更新。调用的方法为commitWork,他会根据Fiber.tag分别处理。
所谓“销毁函数”,见如下例子:
useLayoutEffect(() => {
// ...一些副作用逻辑
return () => {
// ...这就是销毁函数
}
})
HostComponent mutation
当fiber.tag为HostComponent,会调用commitUpdate
最终会在updateDOMProperties中将render阶段 completeWork中为Fiber节点赋值的updateQueue对应的内容渲染在页面上。
for (let i = 0; i < updatePayload.length; i += 2) {
const propKey = updatePayload[i];
const propValue = updatePayload[i + 1];
// 处理 style
if (propKey === STYLE) {
setValueForStyles(domElement, propValue);
// 处理 DANGEROUSLY_SET_INNER_HTML
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
setInnerHTML(domElement, propValue);
// 处理 children
} else if (propKey === CHILDREN) {
setTextContent(domElement, propValue);
} else {
// 处理剩余 props
setValueForProperty(domElement, propKey, propValue, isCustomComponentTag);
}
}
Deletion effect
当Fiber节点含有Deletion effectTag,意味着该Fiber节点对应的DOM节点需要从页面中删除。调用的方法为commitDeletion。
该方法会执行如下操作:
- 递归调用
Fiber节点及其子孙Fiber节点中fiber.tag为ClassComponent的componentWillUnmount生命周期钩子,从页面移除Fiber节点对应DOM节点 - 解绑
ref - 调用useEffect的回调函数
layout阶段
与前两个阶段类似,layout阶段也是遍历effectList,执行函数。
具体执行的函数是commitLayoutEffects。
root.current = finishedWork;
nextEffect = firstEffect;
do {
try {
commitLayoutEffects(root, lanes);
} catch (error) {
invariant(nextEffect !== null, "Should be working on an effect.");
captureCommitPhaseError(nextEffect, error);
nextEffect = nextEffect.nextEffect;
}
} while (nextEffect !== null);
nextEffect = null;
commitLayoutEffects
function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) {
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
// 调用生命周期钩子和hook
if (effectTag & (Update | Callback)) {
const current = nextEffect.alternate;
commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes);
}
// 赋值ref
if (effectTag & Ref) {
commitAttachRef(nextEffect);
}
nextEffect = nextEffect.nextEffect;
}
}
commitLayoutEffects一共做了两件事:
- commitLayoutEffectOnFiber(调用
生命周期钩子和hook相关操作) - commitAttachRef(赋值 ref)
commitLayoutEffectOnFiber
commitLayoutEffectOnFiber方法会根据fiber.tag对不同类型的节点分别处理。
- 对于
ClassComponent,他会通过current === null?区分是mount还是update,调用componentDidMount或componentDidUpdate。
触发状态更新的this.setState如果赋值了第二个参数回调函数,也会在此时调用。
this.setState({ xxx: 1 }, () => {
console.log("i am update~");
});
- 对于
FunctionComponent及相关类型,他会调用useLayoutEffect hook的回调函数,调度useEffect的销毁与回调函数
相关类型指特殊处理后的FunctionComponent,比如ForwardRef、React.memo包裹的FunctionComponent
switch (finishedWork.tag) {
// 以下都是FunctionComponent及相关类型
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block: {
// 执行useLayoutEffect的回调函数
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
// 调度useEffect的销毁函数与回调函数
schedulePassiveEffects(finishedWork);
return;
}
在mutation阶段会执行useLayoutEffect hook的销毁函数。
结合这里我们可以发现,useLayoutEffect hook从上一次更新的销毁函数调用到本次更新的回调函数调用是同步执行的。
而useEffect则需要先调度,在Layout阶段完成后再异步执行。
这就是useLayoutEffect与useEffect的区别。
- 对于
HostRoot,即rootFiber,如果赋值了第三个参数回调函数,也会在此时调用。
ReactDOM.render(<App />, document.querySelector("#root"), function() {
console.log("i am mount~");
});
commitAttachRef
commitLayoutEffects会做的第二件事是commitAttachRef。
function commitAttachRef(finishedWork: Fiber) {
const ref = finishedWork.ref;
if (ref !== null) {
const instance = finishedWork.stateNode;
// 获取DOM实例
let instanceToUse;
switch (finishedWork.tag) {
case HostComponent:
instanceToUse = getPublicInstance(instance);
break;
default:
instanceToUse = instance;
}
if (typeof ref === "function") {
// 如果ref是函数形式,调用回调函数
ref(instanceToUse);
} else {
// 如果ref是ref实例形式,赋值ref.current
ref.current = instanceToUse;
}
}
}
代码逻辑很简单:获取DOM实例,更新ref。
总结
从这篇文章中我们梳理了一遍commit流程,我们要紧紧抓住before mutation和mutation,layout阶段这几个重点,在后面应该会更新diff和Hook的文章,可能要在国庆后了,因为马上要国庆了,所以我可能不想带电脑回家,所以文章也要国庆之后更新了,希望自己能保持一个持续学习的过程了