commit阶段

# 流程概览
上一章最后我们了解,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操作后) 这里 (opens new window)可看commit阶段的完整代码。 当然在before mutation阶段前和layout阶段后还有些额外工作,如useEffect的触发、优先级的重置、ref的绑定/解绑等。
# before mutation之前
do {
// 触发useEffect回调与其他同步任务。由于这些任务可能触发新的渲染,所以这里要一直遍历执行直到没有任务
flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);
// root指 fiberRootNode
// root.finishedWork指当前应用的rootFiber
const finishedWork = root.finishedWork;
// 凡是变量名带lane的都是优先级相关
const lanes = root.finishedLanes;
if (finishedWork === null) {
return null;
}
root.finishedWork = null;
root.finishedLanes = NoLanes;
// 重置Scheduler绑定的回调函数
root.callbackNode = null;
root.callbackId = NoLanes;
let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes);
// 重置优先级相关变量
markRootFinished(root, remainingLanes);
// 清除已完成的discrete updates,例如:用户鼠标点击触发的更新。
if (rootsWithPendingDiscreteUpdates !== null) {
if (
!hasDiscreteLanes(remainingLanes) &&
rootsWithPendingDiscreteUpdates.has(root)
) {
rootsWithPendingDiscreteUpdates.delete(root);
}
}
// 重置全局变量
if (root === workInProgressRoot) {
workInProgressRoot = null;
workInProgress = null;
workInProgressRootRenderLanes = NoLanes;
} else {
}
// 将effectList赋值给firstEffect
// 由于每个fiber的effectList只包含他的子孙节点
// 所以根节点如果有effectTag则不会被包含进来
// 所以这里将有effectTag的根节点插入到effectList尾部
// 这样才能保证有effect的fiber都在effectList中
let firstEffect;
if (finishedWork.effectTag > PerformedWork) {
if (finishedWork.lastEffect !== null) {
finishedWork.lastEffect.nextEffect = finishedWork;
firstEffect = finishedWork.firstEffect;
} else {
firstEffect = finishedWork;
}
} else {
// 根节点没有effectTag
firstEffect = finishedWork.firstEffect;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
可看到,before mutation前主要做一些变量赋值,状态重置的工作。
# layout 之后
const rootDidHavePassiveEffects = rootDoesHavePassiveEffects;
// useEffect相关
if (rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = false;
rootWithPendingPassiveEffects = root;
pendingPassiveEffectsLanes = lanes;
pendingPassiveEffectsRenderPriority = renderPriorityLevel;
} else {}
// 性能优化相关
if (remainingLanes !== NoLanes) {
if (enableSchedulerTracing) {
// ...
}
} else {
// ...
}
// 性能优化相关
if (enableSchedulerTracing) {
if (!rootDidHavePassiveEffects) {
// ...
}
}
// ...检测无限循环的同步任务
if (remainingLanes === SyncLane) {
// ...
}
// 在离开commitRoot函数前调用,触发一次新的调度,确保任何附加的任务被调度
ensureRootIsScheduled(root, now());
// ...处理未捕获错误及老版本遗留的边界问题
// 执行同步任务,这样同步任务不需要等到下次事件循环再执行
// 比如在 componentDidMount 中执行 setState 创建的更新会在这里被同步执行
// 或useLayoutEffect
flushSyncCallbackQueue();
return null;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
这里 (opens new window)可看这段代码。 主要包括三点:
useEffect相关的处理(会在layout阶段详解)- 性能追踪相关(源码里有很多和interaction相关的变量。都和追踪React渲染时间、性能相关,在Profiler API (opens new window)和DevTools (opens new window)中使用。这里 (opens new window)可看到interaction的定义)
- 在
commit阶段会触发一些(如:componentDidXXX)、hook(如:useEffect、useLayoutEffect) 在回调方法中可能触发新的更新,新的更新会开启新的render-commit流程。
# before mutation阶段
before mutation阶段的代码很短,整个过程就是遍历effectList并调用commitBeforeMutationEffects函数处理。
源码在这里 (opens new window)。
// 保存之前的优先级,以同步优先级执行,执行完毕后恢复之前优先级
const previousLanePriority = getCurrentUpdateLanePriority();
setCurrentUpdateLanePriority(SyncLanePriority);
// 将当前上下文标记为CommitContext,作为commit阶段的标志
const prevExecutionContext = executionContext;
executionContext |= CommitContext;
// 处理focus状态
focusedInstanceHandle = prepareForCommit(root.containerInfo);
shouldFireAfterActiveInstanceBlur = false;
// beforeMutation阶段的主函数
commitBeforeMutationEffects(finishedWork);
focusedInstanceHandle = null;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 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;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
整体分三部分:
- 处理
DOM节点渲染/删除后的autoFocus、blur逻辑 - 调用
getSnapshotBeforeUpdate生命周期钩子 - 调度
useEffect
# 调用getSnapshotBeforeUpdate
commitBeforeMutationEffectOnFiber是commitBeforeMutationLifeCycles的别名。
在该方法内会调用getSnapshotBeforeUpdate 这里 (opens new window)可看到此逻辑
从React V16开始,componentWillXXX钩子前增加了UNSAFE_前缀。
原来是Stack Reconciler重构了Fiber Reconciler后,render阶段的任务可能中断/重新开始,对应的组件在render阶段的生命周期钩子(即componentWillXXX)可能触发多次。
这种行为和React V15不一致,所以标记为UNSAFE_。
更详细的解释可参考这里 (opens new window)。
为此,React 提供了替代的生命周期钩子getSnapshotBeforeUpdate。
可看到,getSnapshotBeforeUpdate是在commit阶段内的before mutation阶段调用的,由于commit阶段是同步的,所以不会遇到多次调用的问题。
# 调度useEffect
// 调度useEffect
if ((effectTag & Passive) !== NoEffect) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalSchedulerPriority, () => {
// 触发useEffect
flushPassiveEffects();
return null;
});
}
}
2
3
4
5
6
7
8
9
10
11
可看到,scheduleCallback方法由Scheduler模块提供。用于以某个优先级异步调度一个回调函数。
被异步调度的回调函数就是触发useEffect的方法flushPassiveEffects。
# 如何异步调度
在flushPassiveEffects方法内部会从全局变量rootWithPendingPassiveEffects获取effectList。
在completeWork一节聊到,effectList中保存了需执行副作用的Fiber节点。其中副作用包括:
- 插入
DOM节点(Placement) - 更新
DOM节点(Update) - 删除
DOM节点(Deletion) 除此外,当一个FunctionComponent含有useEffect或useLayoutEffect,他对应的Fiber节点也会被赋值effectTag。 可从这里 (opens new window)看到hook相关的effectTag。 在flushPassiveEffects方法内部会遍历rootWithPendingPassiveEffects(即effectList)执行effect回调函数。
若在此时直接执行,rootWithPendingPassiveEffects === null。
那么rootWithPendingPassiveEffects会在何时赋值呢?
在上一节layout之后的代码片段中会根据rootDoesHavePassiveEffects === true?决定是否赋值rootWithPendingPassiveEffects。
const rootDidHavePassiveEffects = rootDoesHavePassiveEffects;
if (rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = false;
rootWithPendingPassiveEffects = root;
pendingPassiveEffectsLanes = lanes;
pendingPassiveEffectsRenderPriority = renderPriorityLevel;
}
2
3
4
5
6
7
所以整个useEffect异步调用分三步:
before mutation阶段在scheduleCallback中调度flushPassiveEffectslayout阶段后将effectList赋值给rootWithPendingPassiveEffectsscheduleCallback触发flushPassiveEffects,flushPassiveEffects内部遍历rootWithPendingPassiveEffects
# 为什么需要异步调用
与 componentDidMount、componentDidUpdate 不同的是,在浏览器完成布局与绘制之后,传给 useEffect 的函数会延迟调用。这使得它适用于许多常见的副作用场景,比如设置订阅和事件处理等情况,因此不应在函数中执行阻塞浏览器更新屏幕的操作。
可见,useEffect异步执行主要是防止同步执行时阻塞浏览器渲染。
# mutation阶段
类似before mutation阶段,也是遍历effectList,执行函数,只不过这里只晓得是commitMutationEffects。
根据effectTag调用不同的处理函数处理Fiber。
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);
2
3
4
5
6
7
8
9
10
# 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;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
commitMutationEffects会遍历effectList,对每个Fiber节点执行如下三个操作:
- 根据
ContentReset effectTag重置文字节点 - 更新
ref - 根据
effectTag分别处理,其中effectTag包括(Placement|Update|Deletion|Hydrating) 其中Hydrating作为服务端渲染相关。
# Placement effect
当Fiber节点含有Placement effectTag,意味着该Fiber节点对应的DOM节点需插入到页面中。调用commitPlacement (opens new window)方法。
该方法所做的工作分三步:
- 获取父级
DOM节点。其中finishedWork为传入的Fiber节点const parentFiber = getHostParentFiber(finishedWork); // 父级DOM节点 const parentStateNode = parentFiber.stateNode;1
2
3 - 获取
Fiber节点的DOM兄弟节点const before = getHostSibling(finishedWork);1 - 根据DOM兄弟节点是否存在决定调用
parentNode.insertBefore或parentNode.appendChild执行DOM插入操作。// parentStateNode是否是rootFiber if (isContainer) { insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent); } else { insertOrAppendPlacementNode(finishedWork, before, parent); }1
2
3
4
5
6
值得注意的是,getHostSibling(获取兄弟DOM节点)的执行很耗时,当在同一个父Fiber节点下依次执行多个插入操作,getHostSibling算法的复杂度为指数级。
这是由于Fiber节点不只包括HostComponent,所以Fiber树和渲染的DOM树节点并不是一一对应。要从Fiber节点找到DOM节点很可能跨层级遍历。
如下例子:
function Item() {
return <li><li>;
}
function App() {
return (
<div>
<Item/>
</div>
)
}
ReactDOM.render(<App/>, document.getElementById('root'));
2
3
4
5
6
7
8
9
10
11
12
13
对应的Fiber树和DOM树结构为:
// Fiber树
child child child child
rootFiber -----> App -----> div -----> Item -----> li
// DOM树
#root ---> div ---> li
2
3
4
5
6
当在div的子节点Item前插入一个新节点p,即App变为:
function App() {
return (
<div>
<p></p>
<Item/>
</div>
)
}
2
3
4
5
6
7
8
对应的Fiber树和DOM树结构为:
// Fiber树
child child child
rootFiber -----> App -----> div -----> p
| sibling child
| -------> Item -----> li
// DOM树
#root ---> div ---> p
|
---> li
2
3
4
5
6
7
8
9
此时DOM节点p的兄弟节点为li,而Fiber节点 p对应的兄弟DOM节点为:
fiberP.sibling.child
即fiber p的兄弟fiber Item的子fiber li
# Update effect
当Fiber节点含有Update effectTag,意味着该Fiber节点需要更新。调用的方法为commitWork (opens new window),他会根据Fiber.tag分别处理。
这里主要关注FunctionComponent和HostComponent
# FunctionComponent mutation
当fiber.tag为FunctionComponent,会调用commitHookEffectListUnmount (opens new window)。该方法会遍历effectList,执行所有useLayoutEffect hook的销毁函数。
所谓销毁函数,见如下:
useLayoutEffect(() => {
// ...一些副作用逻辑
return () => {
// ...这就是销毁函数
}
})
2
3
4
5
6
7
# HostComponent mutation
当fiber.tag为HostComponent,会调用commitUpdate (opens new window)。
最终会在updateDOMProperties (opens new window)中将render阶段completeWork (opens new window)中为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);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Deletion effect
当Fiber节点含有Deletion effectTag,意味着该Fiber节点对应的DOM节点需从页面中删除。调用commitDeletion (opens new window)
该方法执行如下操作:
- 递归调用
Fiber节点及其子孙Fiber节点中fiber.tag为ClassComponent的componentWillUnmount(opens new window)生命周期钩子。从页面移除Fiber节点对应的DOM节点 - 解绑
ref - 调度
useEffect的销毁函数
# layout阶段
该阶段之所以叫layout,因为该阶段的代码都是在DOM渲染完成(mutation阶段完成)后执行的。
该阶段触发的生命周期钩子和hook可直接访问到已改变后的DOM。
与前两个阶段类似,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;
2
3
4
5
6
7
8
9
10
11
12
13
14
# commitLayoutEffects (opens new window)
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;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
commitLayoutEffects一共做了两件事:
commitLayoutEffectOnFiber(调用生命周期钩子和hook相关操作)commitAttachRef(赋值ref)
# commitLayoutEffectOnFiber (opens new window)
方法原名commitLifeCycles commitLayoutEffectOnFiber方法会根据fiber.tag对不同类型的节点分别处理。
- 对于
ClassComponent,通过current === null?区分是mount还是update,调用componentDidMount(opens new window)或componentDidUpdate(opens new window) 触发状态更新的this.setState若赋值了第二个参数回调函数,也会在此时调用。
this.setState({ xxx: 1 }, () => {
console.log("i am update~");
});
2
3
- 对于
FunctionComponent及相关类型,调用useLayoutEffect hook的回调函数,调度useEffect的销毁与回调函数。switch (finishedWork.tag) { // 以下都是FunctionComponent及相关类型 case FunctionComponent: case ForwardRef: case SimpleMemoComponent: case Block: { // 执行useLayoutEffect的回调函数 commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork); // 调度useEffect的销毁函数与回调函数 schedulePassiveEffects(finishedWork); return; } }1
2
3
4
5
6
7
8
9
10
11
12
13
这里 (opens new window)可看这段源码。
上一节讲Update effect时,mutation阶段会执行useLayoutEffect hook的销毁函数。
结合这里可发现,useLayoutEffect hook从上一次更新的销毁函数调用到本次更新的回调函数调用是同步执行的。
而useEffect则需先调度,在Layout阶段完成后再异步执行。
这就是useEffect与useLayoutEffect的区别。
- 对于
HostRoot,即rootFiber若赋值了第三个参数回调函数,也会在此时调用。ReactDOM.render(<App />, document.querySelector("#root"), function() { console.log("i am mount~"); });1
2
3
# commitAttachRef (opens new window)
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;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
逻辑很简单:获取DOM实例,更新ref。
# current Fiber树切换
至此,整个layout阶段就结束了。
这里 (opens new window)可看到这行代码。
root.current = finishedWork;
在双缓存机制一节中讲过,workInProgress Fiber树在commit阶段完成渲染后会变为current Fiber树。这行代码的作业就是切换fiberRootNode指向current Fiber树。
那么这行代码为什么在这里?(在mutation阶段结束后,layout阶段开始前)。
我们知道componentWillUnmount会在mutation阶段执行。此时current Fiber树还指向前一次更新的Fiber树,在生命周期钩子内获取的DOM还是更新前的。
componentDidMount和componentDidUpdate会在layout阶段执行。此时current Fiber树已指向更新后的Fiber树,在生命周期钩子内获取的DOM就是更新后的。
layout阶段会遍历effectList,依次执行commitLayoutEffects。该方法的主要工作为“根据effectTag调用不同的处理函数处理Fiber并更新ref