render阶段

# 流程概览
render阶段主要任务:创建Fiber节点并构建Fiber树。 render阶段开始于performSyncWorkOnRoot或performConcurrentWorkOnRoot方法的调用。这取决于本次更新时同步更新还是异步更新。
// performSyncWorkOnRoot会调用该方法
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
// performConcurrentWorkOnRoot会调用该方法
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
可看到,它们唯一的区别在于是否调用shouldYield。若浏览器当前帧无剩余时间,shouldYield会中止循环,直到浏览器有空闲时间后再继续遍历。
workInProgress代表当前已创建的workInProgress fiber。
performUnitOfWork方法会创建下一个Fiber节点并赋值给workInProgress,并将workInProgress与已创建的Fiber节点连接起来构成Fiber树。
可在这里 (opens new window)看到workLoopConcurrent的源码。
我们知道Fiber Reconciler 是从Stack Reconciler 重构而来,通过遍历的方式实现可中断的递归。所以performUnitOfWork的工作可分两部分:递和归。
# 递阶段
首先从rootFiber开始向下深度优先遍历。为遍历到的每个Fiber节点调用beginWork方法 (opens new window)。
该方法会根据传入的Fiber节点创建子Fiber节点,并将这两个Fiber节点连接起来。
当遍历到叶子节点(即没有子组件的组件)时就进入归阶段。
# 归阶段
在归阶段会调用completeWork (opens new window)处理Fiber节点。
当某个Fiber节点执行完completeWork,若其存在兄弟Fiber节点,会进入其兄弟Fiber节点的递阶段。
若不存在兄弟Fiber,会进入父级Fiber的归阶段。
递和归阶段会交错执行直到归到rootFiber。至此,render阶段的工作就结束了。
# 例子
function App() {
return (
<div>
i am
<span>KaSong</span>
</div>
)
}
ReactDOM.render(<App />, document.getElementById("root"));
2
3
4
5
6
7
8
9
10
对应的Fiber树结构:
render阶段会依次执行:
1. rootFiber beginWork
2. App Fiber beginWork
3. div Fiber beginWork
4. "i am" Fiber beginWork
5. "i am" Fiber completeWork
6. span Fiber beginWork
7. span Fiber completeWork
8. div Fiber completeWork
9. App Fiber completeWork
10. rootFiber completeWork
2
3
4
5
6
7
8
9
10
可看到没有KaSongFiber的beginWork/completeWork,是因为React只会对只有单一文本子节点的Fiber做特殊处理。
// performUnitOfWork的递归版本
function performUnitOfWork(fiber) {
// 执行beginWork
if (fiber.child) {
performUnitOfWork(fiber.child);
}
// 执行completeWork
if (fiber.sibling) {
performUnitOfWork(fiber.sibling);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# beginWork
可在这里 (opens new window)看到beginWork的定义。
beginWork的工作:传入当前Fiber节点,创建子Fiber节点
// current 当前组件对应的Fiber节点在上一次更新时的Fiber节点,即workInProgress.alternate
// workInProgress 当前组件对应的Fiber节点
// renderLanes 优先级相关
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes
): Fiber | null {
// update时:如果current存在可能存在优化路径,可以复用current(即上一次更新的Fiber节点)
if (current !== null) {
// ...省略
// 复用current
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderLanes,
);
} else {
didReceiveUpdate = false;
}
// mount时:根据tag不同,创建不同的子Fiber节点
switch (workInProgress.tag) {
case IndeterminateComponent:
// ...省略
case LazyComponent:
// ...省略
case FunctionComponent:
// ...省略
case ClassComponent:
// ...省略
case HostRoot:
// ...省略
case HostComponent:
// ...省略
case HostText:
// ...省略
// ...省略其他类型
}
}
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
从双缓存机制一节我们知道,除rootFiber外,组件mount时,由于是首次渲染,是不存在当前组件对应的Fiber节点在上一次更新时的Fiber节点,即mount时current === null。
组件update时,由于之前已mount过,所以current !== null
所以,可通过current === null 来区分组件是处于mount还是update。
基于此,beginWork 的工作可分两部分:
- update时: 若current存在,在满足一定条件时可复用current节点,这样能克隆
current.child作为workInProgress.child,而不需新建workInProgress.child。 - mount时: 除
fiberRootNode外,current === null。会根据fiber.tag不同,创建不同类型的子Fiber节点
# update 时
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
oldProps !== newProps ||
hasLegacyContextChanged() ||
(__DEV__ ? workInProgress.type !== current.type : false)
) {
didReceiveUpdate = true;
} else if (!includesSomeLane(renderLanes, updateLanes)) {
didReceiveUpdate = false;
switch (workInProgress.tag) {
// 省略处理
}
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderLanes,
);
} else {
didReceiveUpdate = false;
}
} else {
didReceiveUpdate = 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
26
可看到,满足如下情况时didReceiveUpdate === false (即可直接复用前一次更新的子Fiber,不需新建子Fiber):
oldProps === newProps && workInProgress.type === current.type,即props与fiber.type不变!includesSomeLane(renderLanes, updateLanes),即当前Fiber节点优先级 不够。
# mount时
当不满足优化路径时,进入第二部分,新建子Fiber。
可看到,根据fiber.tag不同,进入不同类型Fiber的创建逻辑。
// mount时:根据tag不同,创建不同的Fiber节点
switch (workInProgress.tag) {
case IndeterminateComponent:
// ...省略
case LazyComponent:
// ...省略
case FunctionComponent:
// ...省略
case ClassComponent:
// ...省略
case HostRoot:
// ...省略
case HostComponent:
// ...省略
case HostText:
// ...省略
// ...省略其他类型
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
这里 (opens new window)可看到tag对应的组件类型。
对于我们常见的组件类型(FunctionComponent/ClassComponent/HostComponent),会进入reconcileChildren (opens new window)方法。
# reconcileChildren
从名字可看出这是Reconciler模块的核心部分。那么他做了啥?
- 对于
mount的组件 ,会创建新的子Fiber节点 - 对于
update的组件,会将当前组件与上次更新时对应的Fiber节点比较(即Diff算法),将比较的结果生成新Fiber节点
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes
) {
if (current === null) {
// 对于mount的组件
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes,
);
} else {
// 对于update的组件
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes,
);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
从代码可看出,和beginWork一样,也通过current === null区分mount与update。
所以不论走哪个逻辑,最终都会生成新的子Fiber节点并赋值给workInProgress.child,作为本次beginWork的返回值 (opens new window),并作为下次performUnitOfWork执行时workInProgress的传参 (opens new window)。
mountChildFibers与reconcileChildFibers这两个方法的逻辑基本一致,唯一的区别:reconcileChildFibers会为生成的Fiber节点带上effectTag属性,而mountChildFibers不会。
# effectTag
我们知道,render阶段的工作是在内存中进行,当工作结束后会通知Renderer需执行DOM操作。要执行DOM操作的具体类型就保存在fiber.effectTag中。
从这里 (opens new window)可看到effectTag对应的DOM操作。
// DOM需要插入到页面中
export const Placement = /* */ 0b00000000000010;
// DOM需要更新
export const Update = /* */ 0b00000000000100;
// DOM需要插入到页面中并更新
export const PlacementAndUpdate = /* */ 0b00000000000110;
// DOM需要删除
export const Deletion = /* */ 0b00000000001000;
2
3
4
5
6
7
8
通过二进制表示effectTag,可方便使用位操作为fiber.effectTag赋值多个effect。
若要通知Renderer将Fiber节点对应的DOM节点插入页面中,需满足两个条件:
fiber.stateNode存在,即Fiber节点中保存了对应的DOM节点。(fiber.effectTag & Placement) !== 0,即Fiber节点存在Placement effectTag
对于第一点,fiber.stateNode会在completeWork中创建。
对于第二点,假设mountChildFibers也会赋值effectTag,那么可预见mount时,整棵Fiber树所有节点都会有Placement effectTag。那么commit 阶段在执行DOM时每个节点都会执行一次插入操作,这样大量的DOM操作是极低效的。
为解决此问题,在mount时只有rootFiber会赋值Placement effectTag,在commit阶段只执行一次插入操作。
# completeWork
这里可看到completeWork方法定义。
和beginWork类似,completeWork也是针对不同fiber.tag调用不同的逻辑处理。
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
const newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
case IndeterminateComponent:
case LazyComponent:
case SimpleMemoComponent:
case FunctionComponent:
case ForwardRef:
case Fragment:
case Mode:
case Profiler:
case ContextConsumer:
case MemoComponent:
return null;
case ClassComponent: {
// ...省略
return null;
}
case HostRoot: {
// ...省略
updateHostContainer(workInProgress);
return null;
}
case HostComponent: {
// ...省略
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
我们重点看看页面渲染所必须的HostComponent(即原生DOM组件对应Fiber节点)
# 处理HostComponent
和beginWork一样,根据current === null?判断是mount还是update
同时针对HostComponent,判断update时还需考虑workInProgress.stateNode != null ?(即Fiber节点是否存在对应的DOM节点)
case HostComponent: {
popHostContext(workInProgress);
const rootContainerInstance = getRootHostContainer();
const type = workInProgress.type;
if (current !== null && workInProgress.stateNode != null) {
// update的情况
// ...省略
} else {
// mount的情况
// ...省略
}
return null;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# update时
当update时,Fiber节点已经存在对应DOM节点,所以不需生成DOM节点。需要做的主要是处理props,如:
onClick、onChange等回调函数的注册- 处理
style prop - 处理
DANGEROUSLY_SET_INNER_HTML prop - 处理
children prop
可看到主要逻辑是调用updateHostComponent (opens new window)方法。
if (current !== null && workInProgress.stateNode != null) {
// update的情况
updateHostComponent(
current,
workInProgress,
type,
newProps,
rootContainerInstance,
);
}
2
3
4
5
6
7
8
9
10
在updateHostComponent内部,被处理完的props会被赋值给workInProgress.updateQueue,并最终会在commit阶段被渲染在页面上。
workInProgress.updateQueue = (updatePayload: any);
其中updatePayload为数组形式,他的偶数索引的值为变化的prop key,奇数索引的值为变化的prop value。
具体渲染过程见mutation阶段。
# mount时
mount时的主要逻辑包括三个:
- 为
Fiber节点生成对应的DOM节点 - 将子孙
DOM节点插入刚生成的DOM节点中 - 与update逻辑中的
updateHostComponent类似的处理props的过程
// mount的情况
// ...省略服务端渲染相关逻辑
const currentHostContext = getHostContext();
// 为fiber创建对应DOM节点
const instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);
// 将子孙DOM节点插入刚生成的DOM节点中
appendAllChildren(instance, workInProgress, false, false);
// DOM节点赋值给fiber.stateNode
workInProgress.stateNode = instance;
// 与update逻辑中的updateHostComponent类似的处理props的过程
if (
finalizeInitialChildren(
instance,
type,
newProps,
rootContainerInstance,
currentHostContext,
)
) {
markUpdate(workInProgress);
}
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
mount时只会在rootFiber存在Placement effectTag。那么commit阶段是如何通过一次插入DOM操作(对应一个Placement effectTag)将整棵DOM树插入页面的呢?
原因就在于completeWork中的appendAllChildren方法。
由于completeWork属于“归”阶段调用的函数,每次调用appendAllChildren时都会将已生成的子孙DOM节点插入当前生成的DOM节点下。那么当“归”到rootFiber时,我们已经有一个构建好的离屏DOM树。
# effectList
至此,render阶段的绝大部分工作已完成。
还要一个问题:作为DOM操作的依据,commit阶段需找到所有有effectTag的Fiber节点并依次执行effectTag对应操作。难道要在commit阶段再遍历一次Fiber树寻找effectTag !== null的Fiber节点?
这显然很低效。
为解决此问题,在completeWork的上层函数completeUnitOfWork中,每个执行完completeWork且存在effectTag的Fiber节点会被保存在一条effectList的单向链表中。
effectList中第一个Fiber节点保存在fiber.firstEffect,最后一个元素保存在fiber.lastEffect。
类似appendAllChildren,在归阶段,所有有effectTag的Fiber节点都会被追加在effectList中,最终形成一条rootFiber.firstEffect为起点的单向链表。
nextEffect nextEffect
rootFiber.firstEffect -----------> fiber -----------> fiber
2
这样,在commit阶段只需遍历effectList就能执行所有effect了。
可在这里 (opens new window)看到这段代码。
effectList相较于Fiber树,就像圣诞树上挂的那一串彩灯。
至此,render阶段全部工作完成。在performSyncWorkOnRoot函数中fiberRootNode被传递给commitRoot方法,开启commit阶段工作流程。
commitRoot(root)