React 源码漂流记:React 调和器核心源码解读(五)
# 目录
# 前言
在上文中,我们探讨了在 beginWork
函数中针对不同的类型(内部类型)的 React 组件所采取的不同的调和策略:复用和重新调和。在本文中,我们继续来探讨在捕获阶段中组件的复用机制以及组件的具体的调和过程。本文将主要围绕着 cloneChildFibers
和 reconcileChildren
两个核心函数展开探讨。
# bailoutOnAlreadyFinishedWork
通过上文分析,我们已经了解了无论是函数式组件还是类组件,在提前退出时都是调用 bailoutOnAlreadyFinishedWork
函数。要想深入了解 cloneChildFibers
的作用,我们可能要先了解此函数的原理。
// src/react/packages/react-reconciler/src/ReactFiberBeginWork.new.js
function bailoutOnAlreadyFinishedWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
// ......
markSkippedUpdateLanes(workInProgress.lanes);
// Check if the children have any pending work.
// 检查子树是否有相应的更新,通过 childLanes 判断,如果没有更新,则可以提前结束子树的捕获过程,并开始向上冒泡
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
// The children don't have any work either. We can skip them.
return null;
}
// This fiber doesn't have work, but its subtree does. Clone the child
// fibers and continue.
// 子树有更新,复制子树的 fibers 并返回子节点继续捕获
cloneChildFibers(current, workInProgress);
return workInProgress.child;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
分析如下:
markSkippedUpdateLanes
函数将本次渲染期间累计的未处理的更新的 lanes 合并到workInProgress.lanes
中。这是因为在调度到真正渲染期间,可能会有新的 lanes 被收集到。- 这里有一个性能的优化,如果
renderLanes
中不包含workInProgress
节点的childLanes
,即子 FiberTree 没有更新,则可以通过返回null
,提前结束捕获。
# cloneChildFibers
此函数将复用当前 Fiber 节点的所有下级子节点。
export function cloneChildFibers(
current: Fiber | null,
workInProgress: Fiber,
): void {
// ......
let currentChild = workInProgress.child;
let newChild = createWorkInProgress(currentChild, currentChild.pendingProps);
workInProgress.child = newChild;
newChild.return = workInProgress;
while (currentChild.sibling !== null) {
currentChild = currentChild.sibling;
newChild = newChild.sibling = createWorkInProgress(
currentChild,
currentChild.pendingProps,
);
newChild.return = workInProgress;
}
newChild.sibling = null;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
下图是相关的数据结构的操作逻辑:
分析如下:
createWorkInProgress
创建新的workInProgress
fiber 节点,并复用current FiberNode
的属性(如果workInProgress
节点是悬空的,则创建 Fiber 节点,见createFiber
函数)。newChild
指针不断向兄弟节点移动,直至遍历完所有的兄弟节点。
# reconcileChildren
此函数根据是否是首次渲染而决定是否追踪副作用,并且调用相应的函数对姐姐点进行调和。
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes,
) {
if (current === null) {
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes,
);
} else {
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes,
);
}
}
const reconcileChildFibers = ChildReconciler(true);
const mountChildFibers = ChildReconciler(false);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
分析如下:
- 如果是首次渲染,则不必追踪副作用,调用
mountChildFibers
,否则则调用reconcileChildFibers
。主要这两个函数都是调用包装函数ChildReconciler
实现的。 reconcileChildren
调和子节点,并赋值给workInProgress.child
。
# reconcileChildFibers
此函数对 workInProgress
节点的 children
节点进行调和,并且标记副作用。
// src/react/packages/react-reconciler/src/ReactChildFiber.new.js
// This API will tag the children with the side-effect of the reconciliation
// itself. They will be added to the side-effect list as we pass through the
// children and the parent.
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
// 处理 Element Fragment 解包装,类型为 REACT_FRAGMENT_TYPE
const isUnkeyedTopLevelFragment =
typeof newChild === 'object' &&
newChild !== null &&
newChild.type === REACT_FRAGMENT_TYPE &&
newChild.key === null;
if (isUnkeyedTopLevelFragment) {
newChild = newChild.props.children;
}
// 非文本节点,包括数组和迭代器
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return placeSingleChild(reconcileSingleElement(/*...*/));
case REACT_PORTAL_TYPE:
return placeSingleChild(reconcileSinglePortal(/*...*/));
case REACT_LAZY_TYPE:
return reconcileChildFibers(/*...*/);
}
if (isArray(newChild)) {
return reconcileChildrenArray(/*...*/);
}
if (getIteratorFn(newChild)) {
return reconcileChildrenIterator(/*...*/);
}
// ......
}
// 如果是文本节点
if (
(typeof newChild === 'string' && newChild !== '') ||
typeof newChild === 'number'
) {
return placeSingleChild(reconcileSingleTextNode(/*...*/));
}
// 如果不符合上述情况,视为清空子节点
return deleteRemainingChildren(returnFiber, currentFirstChild);
}
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
分析如下:
- 明确
newChild
是什么?newChild
本质上是 ReactElement,是React.createElement
语法糖创建出来的对象,具体的结构可见参见 ReactElement 与基础概念 (opens new window)。 - React.Fragment 解包装之后成为节点链表。
- 本函数处理合法的节点类型(注意,此处不是 Fiber 类型,而是 ReactElement 类型,根据
ReactElement.$$typeof
判断):普通 Element、Portal、Lazy 节点、节点链表、节点迭代器、文本节点,其余的形式均视为非法清空所有子节点。 - 从整体来看,子节点的处理分成两步:调和(reconcile)和置位(place)。调和的过程就是创建 workInProgress FiberTree 的 Fiber 节点的过程,而置位就是进行副作用标记的过程。
- 对于 Lazy 节点,即延迟加载的节点,解包装出延迟的组件后,递归调用自身即可。
- 注意:
reconcileChildFibers
返回调和后的第一个子节点。 reconcileChildFibers
实现了从 ReactElement 到 Fiber 的飞跃。从入参newChild
是 ReactElement,到返回值为 Fiber 可见一斑。
下面我们分别来探讨下 Element 节点链表、 REACT_ELEMENT_TYPE
单一节点和 Element 文本节点的具体原理。
# reconcileChildrenArray
此函数对同级子节点应用 DIFF 算法,目的是通过 DIFF 算法根据 current Fiber 和 ReactElement 高效的创建 workInProgress Fiber。此过程可视为节点链表的调和过程。
注意
相关简写:
- 原链表:current 节点链表。
- 新数组:ReactElement 节点数组。
- 原节点:current 节点链表中对应位置的节点(Fiber)。
- 新节点:ReactElement 节点链表中对应位置的节点(ReactElement)。
- 结果节点:经过 DIFF 后要置位于对应位置的节点(Fiber)。
具体过程如下:
// src/react/packages/react-reconciler/src/ReactChildFiber.new.js
function reconcileChildrenArray(
// workInProgress Fiber
returnFiber: Fiber,
// current Fiber 上的第一个子节点
currentFirstChild: Fiber | null,
// 当前调和中的 ReactElement
newChildren: Array<*>,
// 调和节点的优先级
lanes: Lanes,
): Fiber | null {
// ......
// 调和后的第一个子节点
let resultingFirstChild: Fiber | null = null;
// 上一个被置位的结果节点
let previousNewFiber: Fiber | null = null;
// 原节点指针,指向 current 节点链表中的节点
let oldFiber = currentFirstChild;
// 上一次置位的下标
let lastPlacedIndex = 0;
let newIdx = 0;
// 下一轮要比较的原节点指针
let nextOldFiber = null;
// 以下取原链表和新数组较短的长度的部分进行 DIFF(循环原链表和新数组,知道两者之一没有更多节点)
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
// 如果原节点的下标与当前置位的下标不一致,说明可能是在原节点的前面添加了新节点,因此
// 将原节点推迟到下一次比较,本次比较原节点则悬空
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
// 如果下标一致,则下一个要比较的原节点则为当前原节点的兄弟节点
nextOldFiber = oldFiber.sibling;
}
// 根据当前比较,尝试从原链表中复用节点。
// 根据 children 的类型进行匹配,如果 key 值匹配则返回结果节点,否则返回 null。
const newFiber = updateSlot(
returnFiber,
// 本次比较的原节点,可能是 null
oldFiber,
// 本次比较的新节点
newChildren[newIdx],
lanes,
);
// 如果没有节点可复用,则跳出循环
if (newFiber === null) {
if (oldFiber === null) {
oldFiber = nextOldFiber;
}
break;
}
// 如果节点可以复用,且非初次渲染,原节点存在且未与新节点建立链接,则删除原节点
if (shouldTrackSideEffects) {
if (oldFiber && newFiber.alternate === null) {
deleteChild(returnFiber, oldFiber);
}
}
// 将更新后的新节点置位
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
// 首个结果节点链接在 resultingFirstChild 上,后续结果结果连接在上个节点的 sibling 指针上
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
// 原节点指针移动到下一个原节点
oldFiber = nextOldFiber;
}
// 如果经过上述复用之后已经没有更多新节点,DIFF 结束,删除剩余的原节点,并返回结果节点首节点
if (newIdx === newChildren.length) {
deleteRemainingChildren(returnFiber, oldFiber);
// ......
return resultingFirstChild;
}
// 如果没有更多原节点,后续结果无需 DIFF,可直接置位,并返回结果节点首节点
if (oldFiber === null) {
// 循环剩余的新节点
for (; newIdx < newChildren.length; newIdx++) {
// 根据新节点创建子节点
const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
if (newFiber === null) {
continue;
}
// 对子节点进行置位
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
// 将子节点链接到结果节点链表
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
// ......
return resultingFirstChild;
}
// 如果经过上述 DIFF,原节点和新节点均有剩余,说明提前退出了 DIFF 过程(遇到 key 值不匹配)
// 则更换 DIFF 策略,采用节点池的方式复用节点
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
// 循环剩余的待置位的新节点,寻求节点复用
for (; newIdx < newChildren.length; newIdx++) {
// 尝试从节点池复用节点
// 注意:这里仍然是 key 值匹配才可以复用,如果匹配不到则创建节点
const newFiber = updateFromMap(
existingChildren,
returnFiber,
newIdx,
newChildren[newIdx],
lanes,
);
// 如果有可复用节点(或者创建出节点),则复用节点并置位
if (newFiber !== null) {
if (shouldTrackSideEffects) {
// 非首次渲染时,如果该节点已经与原节点链接,则删除原节点
if (newFiber.alternate !== null) {
existingChildren.delete(
newFiber.key === null ? newIdx : newFiber.key,
);
}
}
// 对该节点进行置位,并链接到结果节点链表
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
if (previousNewFiber === null) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
// 删除未被消费的原节点
if (shouldTrackSideEffects) {
existingChildren.forEach(child => deleteChild(returnFiber, child));
}
// ......
// 返回首个结果节点
return resultingFirstChild;
}
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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
分析如下:
- DIFF 过程总结如下:将相应下标的原节点用于新节点进行 DIFF(严格匹配 key 值),直到原链表或者新数组没有更多节点,或者首次遇到因无可匹配 key 值(
newChild.key === key
,考虑各自为 null 的情况)而无法复用节点的情况提前循环。退出循环分三种情况:如果没有更多的新节点,DIFF 结束,删除剩余的原节点,并返回结果节点首节点;如果没有更多原节点,后续结果无需 DIFF,可直接创建节点并置位,并返回结果节点首节点;如果原节点和新节点均有剩余,则采用节点池的方式复用节点,复用节点或者创建节点。 - 节点的 DIFF 本质上是对同层级的节点的 DIFF。与 vue3 的 DIFF 算法相比,可能 vue3 的 DIFF 的效率要更高一些。React 目前的 DIFF 算法没有办法采用更高效的双向搜索(从两端同时 DIFF)的方式,这是因为 React 中同级子节点是以单向链表管理的,节点之间用
sibling
指针链接,没有反向指针导致节点无法反向回溯。那么为什么 Vue3 可以做到双向搜索呢?这是因为 vue3 的同级子节点使用数组管理的,数组原生支持正向和反向的遍历。 - 为什么不合法的 key 值会在控制台打印警告?函数中
warnOnInvalidKey
会检查不合法的 key 值,包括重复的 key 值,并且打印警告信息。 - 为什么 key 值很重要?因为 key 值对于复用节点很重要!更重 DIFF 算法都依赖 key 以复用原节点,以节省创建节点的内存的开销。
# reconcileSingleElement
此函数对单一 Element 子节点进行调和。
// src/react/packages/react-reconciler/src/ReactChildFiber.new.js
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes,
): Fiber {
const key = element.key;
let child = currentFirstChild;
// 遍历原节点链表寻求能够被新节点复用的原节点
while (child !== null) {
// 如果有key值匹配的原节点
if (child.key === key) {
const elementType = element.type;
// 如果 Element 是 Fragment,且可复用原节点也是 Fragment
// 在 reconcileChildFibers 中 Element 已经解包装过一次
if (elementType === REACT_FRAGMENT_TYPE) {
if (child.tag === Fragment) {
// 删除剩余的所有的节点,因为可复用的 child 已经缓存
deleteRemainingChildren(returnFiber, child.sibling);
// 复用 child 并对 Element 再做一次解包装,注意这里的复用节点会复用 child 和 sibling 指针
const existing = useFiber(child, element.props.children);
// 将 return 指针指向 returnFiber
existing.return = returnFiber;
// ......
return existing;
}
} else {
// 如果 Element 不是 Fragment,且可复用原节点的类型和 Element 相同
if (child.elementType === elementType /*......*/) {
// 删除剩余的所有节点
deleteRemainingChildren(returnFiber, child.sibling);
// 复用原节点
const existing = useFiber(child, element.props);
// 处理 string refs
existing.ref = coerceRef(returnFiber, child, element);
// 将 return 指针指向 returnFiber
existing.return = returnFiber;
// ......
return existing;
}
}
// Didn't match.
deleteRemainingChildren(returnFiber, child);
break;
} else {
// 旧节点如果不能被复用直接删除
deleteChild(returnFiber, child);
}
// 指针移到兄弟节点
child = child.sibling;
}
// 如果没有可复用的节点,根据 Element 类型创建新节点(Fiber),并将之返回
if (element.type === REACT_FRAGMENT_TYPE) {
const created = createFiberFromFragment(/*......*/);
created.return = returnFiber;
return created;
} else {
const created = createFiberFromElement(element, returnFiber.mode, lanes);
created.ref = coerceRef(returnFiber, currentFirstChild, element);
created.return = returnFiber;
return created;
}
}
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
分析如下:
- coerceRef 处理
String Refs
,参考 Legacy API: String Refs (opens new window)。 - 对于单一单一 Element 子节点,在调和时需要根据 Element 的类型(Fragment 或者 普通 Element)采用不同的调和策略,如果有旧节点可复用,在调用
useFiber
复用之,如果没有则createFiberFromFragment
或者createFiberFromElement
创建新节点。
# reconcileSingleTextNode
此函数对单一 Element 文本子节点进行调和。
function reconcileSingleTextNode(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
textContent: string,
lanes: Lanes,
): Fiber {
// 如果原节点的首个节点刚好是文本节点,则删除其余的节点,并且复用这个文本节点
if (currentFirstChild !== null && currentFirstChild.tag === HostText) {
deleteRemainingChildren(returnFiber, currentFirstChild.sibling);
const existing = useFiber(currentFirstChild, textContent);
existing.return = returnFiber;
return existing;
}
// 如果首个节点不是文本节点,则删除所有节点,创建新节点
deleteRemainingChildren(returnFiber, currentFirstChild);
const created = createFiberFromText(textContent, returnFiber.mode, lanes);
created.return = returnFiber;
return created;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
因为这里 Element 是文本类型,所以调用 createFiberFromText
函数创建 FIber 节点。需要注意的是,创建的新节点需要把 return
指针指向父节点。
# placeSingleChild
此函数对 reconcileSingleElement
创建的节点进行置位(标记副作用,Effect Tag)。
// src/react/packages/react-reconciler/src/ReactChildFiber.new.js
function placeSingleChild(newFiber: Fiber): Fiber {
// 添加 Placement 标记
if (shouldTrackSideEffects && newFiber.alternate === null) {
newFiber.flags |= Placement;
}
return newFiber;
}
2
3
4
5
6
7
8
# updateSlot
此函数对 reconcileChildrenArray
中可复用的节点进行更新复用。
function updateSlot(
returnFiber: Fiber,
oldFiber: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
const key = oldFiber !== null ? oldFiber.key : null;
// 如果是文本节点则调用 updateTextNode 复用节点
if (
(typeof newChild === 'string' && newChild !== '') ||
typeof newChild === 'number'
) {
// ......
return updateTextNode(returnFiber, oldFiber, '' + newChild, lanes);
}
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
if (newChild.key === key) {
return updateElement(returnFiber, oldFiber, newChild, lanes);
} else {
return null;
}
}
case REACT_PORTAL_TYPE: {
if (newChild.key === key) {
return updatePortal(returnFiber, oldFiber, newChild, lanes);
} else {
return null;
}
}
case REACT_LAZY_TYPE: {
const payload = newChild._payload;
const init = newChild._init;
return updateSlot(returnFiber, oldFiber, init(payload), lanes);
}
}
// 如果是 Fragment 或者是可迭代对象
if (isArray(newChild) || getIteratorFn(newChild)) {
// ......
return updateFragment(returnFiber, oldFiber, newChild, lanes, null);
}
// ......
}
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
44
45
46
47
复用的策略可以总结如下:
Text | Element | Portal | Lazy | Fragment | |
---|---|---|---|---|---|
方法 | updateTextNode | updateElement | updatePortal | updateSlot | updateFragment |
注意这里是严格比对 key 值相等,否则一律返回 null。对于具体的复用细节,后文详述。
# updateFromMap
此函数对 reconcileChildrenArray
中节点池中可复用的节点进行更新复用。具体逻辑与 updateSlot
一致,不在赘述。
# placeChild
此函数对 reconcileChildrenArray
中调和的节点进行置位。
function placeChild(
newFiber: Fiber,
lastPlacedIndex: number,
newIndex: number,
): number {
// 缓存下标共下次调和使用
newFiber.index = newIndex;
const current = newFiber.alternate;
// ......
// current 存在说明 current 即是将被复用的原节点
// 如果非首次渲染,比较原节点与新节点的下标,如果新节点在原节点的后面,标记为 Placement,并返回置位的下标
// 如果新节点不在原节点的后面,则位置无需变化,保持原位置即可
if (current !== null) {
const oldIndex = current.index;
if (oldIndex < lastPlacedIndex) {
newFiber.flags |= Placement;
return lastPlacedIndex;
} else {
return oldIndex;
}
} else {
// 没有被复用的节点,添加 Placement 标记
newFiber.flags |= Placement;
return lastPlacedIndex;
}
}
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
# deleteChild
此函数收集待删除的节点并且对 returnFiber
添加 ChildDeletion
副作用标记。
function deleteChild(returnFiber: Fiber, childToDelete: Fiber): void {
// 初次渲染不需要 ChildDeletion 标记
if (!shouldTrackSideEffects) return;
// 将待删除的节点加入到 returnFiber.deletions 数组中,并且在 returnFiber 上添加 ChildDeletion 标记
const deletions = returnFiber.deletions;
if (deletions === null) {
returnFiber.deletions = [childToDelete];
returnFiber.flags |= ChildDeletion;
} else {
deletions.push(childToDelete);
}
}
2
3
4
5
6
7
8
9
10
11
12
# 扩展
# 函数式组件和类组件在 reconcileChildren
之前做了什么?
本文中只大致介绍了函数式组件和类组件在捕获时调和过程的大致脉络,这个过程最终都会分成 cloneChildFibers
和 reconcileChildren
,而对于其中的细节只是一笔带过,如 renderWithHooks
、 constructClassInstance
、 mountClassInstance
、 updateClassInstance
、 finishClassComponent
等函数,这些逻辑跟相应的组件的关联比较大,与整个的调和过程关系不大,因此这部分内容将在后文组件机制的文章中陆续展开探讨。
# workInProgress Tree 是如何初始化的?
请看下面这个简易的获取冒泡的过程:
可以看出,在 WorkLoop
的捕获和冒泡过程中, workInProgress
指针是在不停移动的。在应用挂载时,进入对 workInProgress
指针所指向的节点的 beginWork
过程会调用 reconcileChildren
函数,此函数判断到为挂载(mount)阶段则会在 reconcileChildFibers
中创建 workInProgress Fiber。
# 问题
# 总结
本文介绍了 React 调和过程中冒泡阶段对 workInProgress 节点(换一种角度看也是子节点)进行调和的原理细节,包括子节点的创建和复用、DIFF 算法、副作用标记(Effect Tag)等。从整体上来看,workInProgress 的调和过程与 ReactElement 的抽象层是密不可分的,Fiber 的调和过程依赖于 ReactElement 对应用最新状态的榨取,这些最新的状态最终从内存中被收集起来,成为 Fiber 上可回溯的数据。整个捕获过程除了创建 workInProgress Fiber 之外,还担负着标记副作用的职责。副作用将为 DOM 的更新起到指导作用。
有以下几点还需注意:
- React DIFF 算法虽然在一定程度上已经做了一些优化,但是由于数据结构本身的限制,还是有一定的可优化的空间。
- React 的更新粒度是 FiberRoot 为基础,虽然在调和期间有一定的优化措施,比如各种节点复用,甚至是提前跳出捕获过程,但是相对于 Vue3 中基于模板分析的组件粒度的更新机制,还是有一定的性能差距的。
- React 中的 VDOM 如何理解?这里的 VDOM 不只是 ReactElement,还应该包括 FiberTree。这是因为 ReactElement 对于维持组件状态很重要,但是 React 应用的调度、更新和渲染离不开 FiberTree 的结构。因此可以将 ReactElement 理解为组件状态层面的 VDOM,而把 FiberTree 理解为应用层面上的 VDOM。
- 本文中已经接触到的副作用包括
Placement
、ChildDeletion
和ContentReset
。从理论上来说,DOM 的更新只需要两个副作用,“更新”(重新调整位置,包括插入)、“删除”。在后文中,我们将针对更新的副作用进行更深入的探究。此处Placement
意即更新节点位置,ChildDeletion
意即删除节点,ContentReset
意即针对可视为文本节点HostComponent
节点重置其文本内容。