React源码-commit核心
# 0. 前言
此部分是承接上部分 commit
阶段前后工作的。
单拎出来分析的原因,before mutation
、mutation
和 layout
其中用到的函数在 react-recomciler/ReactFiberCommitWOrk.js
文件中。
# 1. 阶段
Renderer
工作的阶段被称为commit
阶段。commit
阶段可以分为三个子阶段:
- before mutation 阶段(执行
DOM
操作前) - mutation 阶段(执行
DOM
操作) - layout 阶段(执行
DOM
操作后)
这三个阶段的函数有个共性的点,就是会循环遍历 effectList
链表,伪代码如下:
const nextEffect = effectList.firstEffect;
do {
// effectList 会被遍历一遍,三个阶段就会被遍历三遍
nextEffect = nextEffect.nextEffect;
}while(nextEffect !== null);
2
3
4
5
接下来我们来对每个阶段进行具体的分析。
# 2. BeforeMutation
第一个 do...while
语句,伪代码如下:
do {
try {
commitBeforeMutationEffects();
} 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
核心处理逻辑在 commitBeforeMutationEffects
function commitBeforeMutationEffects() {
while (nextEffect !== null) {
const current = nextEffect.alternate;
// 1. 处理`DOM节点`渲染/删除后的 `autoFocus`、`blur`逻辑【省略】
// 2. 调用`getSnapshotBeforeUpdate`生命周期钩子
// 3. 调度`useEffect`
// 持续读取 nextEffect 链表
nextEffect = nextEffect.nextEffect;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
递归遍历的逻辑:nextEffect = nextEffect.nextEffect;
# 2.1 处理 Blur 事件(省略)
这部分没啥意义,可以跳过。
# 2.2 处理 Class
组件的生命周期
预先知识复习:
getSnapshotBeforeUpdate(prevProps,preState)
方法在最近一次渲染输出(提交到 DOM 节点)之前调用。用过代码就能理解,此时获取的值为
current
树上的props
和state
值。const prevProps = current.memoizedProps; const prevState = current.memoizedState;
1
2这里
current
树为当前页面展示的树,最新的值是保存在workInProgress
中的,这也就是取名为啥叫prevState
和prevProps
的原因getSnapshotBeforeUpdate()
方法需要与componentDidUpdate()
方法一起使用,否则会出现错误。生命周期执行次序:
getSnapshotBeforeUpdate
→componentDidUpdate
getSnapshotBeforeUpdate(prevProps,preState){ console.log("#enter getSnapShotBeforeUpdate"); return null; } // snapshot 为上述周期的返回值 componentDidUpdate(prevProps, prevState, snapshot){ console.log("#enter componentDidUpdate snapshot = ", snapshot); }
1
2
3
4
5
6
7
8
9
判断是否具有 Snapshot
副作用,若存在则调用 commitBeforeMutationEffectOnFiber()
const flags = nextEffect.flags;
if ((flags & Snapshot) !== NoFlags) {
commitBeforeMutationEffectOnFiber(current, nextEffect);
}
2
3
4
具体针对的是 Class Component
组件:
function commitBeforeMutationLifeCycles(
current: Fiber | null,
finishedWork: Fiber,
): void {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block: {
return;
}
case ClassComponent: {
if (finishedWork.flags & Snapshot) {
if (current !== null) {
const prevProps = current.memoizedProps;
const prevState = current.memoizedState;
const instance = finishedWork.stateNode;
// 执行 getSnapshotBeforeUpdate(preProps,preState);
const snapshot = instance.getSnapshotBeforeUpdate(
finishedWork.elementType === finishedWork.type
? prevProps
: resolveDefaultProps(finishedWork.type, prevProps),
prevState,
);
instance.__reactInternalSnapshotBeforeUpdate = snapshot;
}
}
return;
}
}
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
# 2.3 处理 function
组件上的 useEffect
// 判断当前函数是否使用 useEffect
if ((flags & Passive) !== NoFlags) {
// 当 root 上没有标识时,修改全局变量为 true
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
// 使用 schedule 包对 useEffect 内容进行调度【该包可以独立拆分使用】
scheduleCallback(NormalSchedulerPriority, () => {
// 触发useEffect
flushPassiveEffects();
return null;
});
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# 3. Mutation 阶段
这部分是核心中的核心:如何操作 DOM
元素,内部核心调用底层的 DOM API
对页面进行视图更新。
首先依旧是 do...while()
循环,其中涉及到报错处理:
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
核心处理逻辑在 commitBeforeMutationEffects
function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
// 遍历 EffectList 链表
while (nextEffect !== null) {
const current = nextEffect.alternate;
// 1. 根据 ContentReset effectTag重置文字节点
// 2. 更新ref
// 3. 根据 effectTag 分别处理
const primaryEffectTag =
effectTag & (Placement | Update | Deletion | Hydrating);
switch (primaryEffectTag) {
case Placement: {
nextEffect.effectTag &= ~Placement;
break;
}
case PlacementAndUpdate: {
nextEffect.effectTag &= ~Placement;
break;
}
case Hydrating: {
nextEffect.effectTag &= ~Hydrating;
break;
}
case HydratingAndUpdate: {
nextEffect.effectTag &= ~Hydrating;
break;
}
case Update: {
}
case Delection: {
}
}
// 持续读取 nextEffect 链表
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
递归遍历的逻辑:nextEffect = nextEffect.nextEffect;
# 3.1 重置文本节点
const flags = nextEffect.flags;
if (flags & ContentReset) {
commitResetTextContent(nextEffect);
}
2
3
4
5
# 3.2 处理 ref
属性
暂时不纠结这块内容
# 3.3 如何处理插入 DOM
元素
当 EffectTag
为 Placement
时,触发 commitPlacement(nextEffect)
函数。
!注意:此函数来源于
ReactFiberCommitWork.js
文件中。
这边的难点在于 DOM
结构与 Fiber
结构不一致,导致过程相当复杂,举例如下:
想要在 App
的 <div />
内插入 <p />
结构:
function Item() {
return <li></li>;
}
function App() {
return (
<div>
+ <p></p>
<Item/>
</div>
)
}
// DOM树(旧)
#root ---> div ---> li
// DOM树(新)
#root ---> div ---> p
|
---> li
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
虽然只插入一个节点,但是对于 Fiber
结构确是个大改动。
// Fiber树 - 改动前:
child child child child
rootFiber -----> App[×] -----> div -----> Item[×] -----> li
// Fiber树 - 改动后:
child child child
rootFiber -----> App[×] -----> div -----> p
| sibling[+] child
| -------> Item[x] -----> li
2
3
4
5
6
7
8
9
10
EffectTag
为 Placement
状态分析:
function Item() {
const [isShow, setIsShow] = useState(false);
return (
<>
{isShow && <p>+p Item</p>}
<li onClick={() => setIsShow(true)}>Item</li>
</>
);
}
function App() {
/* const [isShow, setIsShow] = useState(false); */
return (
<div>
{/* {isShow && <p>+p</p>} */}
<Item />
</div>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
根据断点分析如下:
> firstEffect
FiberNode {tag: 5, key: null, elementType: 'p', type: 'p', stateNode: p, …}
> firstEffect.nextEffect
FiberNode {tag: 5, key: null, elementType: 'li', type: 'li', stateNode: li, …}
2
3
4
对于以上情况来说,在更新阶段,为
<p />
下一个更新节点为兄弟节点<li />
若 JSX
为如下:
function Item() {
return (
<>
<li>Item</li>
</>
);
}
function App() {
const [isShow, setIsShow] = useState(false);
return (
<div onClick={() => setIsShow(true)}>
{isShow && <p>+p</p>}
<Item />
</div>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
根据断点分析 update
阶段生成的 EffectList
链表如下:
> firstEffect
FiberNode {tag: 5, key: null, elementType: 'p', type: 'p', stateNode: p, …}
> firstEffect.nextEffect
FiberNode {tag: 5, key: null, elementType: 'div', type: 'div', stateNode: div, …}
2
3
4
第一个节点为
<p />
标签,以下一个节点为父节点<div />
接下来逻辑如下:
在
ReactFiberWorkLoop
文件中:function commitMutationEffects( switch (primaryFlags) { case Placement: { commitPlacement(nextEffect/*EffectList 链表*/); } } }
1
2
3
4
5
6
7在
ReactFiberCommitWork.js
文件中的commitPlacement
函数:核心是对以下三个变量的处理:
parentDOM
、siblingDOM
、selfDOM
function commitPlacement(finishedWork: Fiber): void { const parentFiber = getHostParentFiber(finishedWork); // 找到并取出具有 DOM 结构的父组件 const parentInstance = parentFiber.stateNode; // 当前 EffectList 自身是否具有 DOM 结构 const isHost = tag === HostComponent || tag === HostText; // 当前 NextEffect 的 DOM 结构 const childInstance = isHost ? node.stateNode : node.stateNode; // 找到 NextEffect 的兄弟组件 const beforeInstance = getHostSibling(finishedWork); if (before) { parentStateNode.insertBefore(childInstance, beforeInstance); } else { parentStateNode.appendChild(childInstance); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16PS:以上代码和源码出入较大,但是核心逻辑就是如此。
# 备注
scheduleCallback
与runWithPriority
有什么区别?flushPassiveEffect
中存了什么?flushSyncCallbackQueue
的区别?