1. performUnitOfWork
render阶段开始于performSyncWorkOnRoot
或performConcurrentWorkOnRoot
方法的调用。这取决于本次更新是同步更新还是异步更新。
// performSyncWorkOnRoot会调用该方法
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
// performConcurrentWorkOnRoot会调用该方法
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
两种方法区别在于是否调用shouldYield
。如果当前浏览器帧没有剩余时间,shouldYield
会中止循环,直到浏览器有空闲时间后再继续遍历。
workInProgress
代表当前已创建的workInProgress fiber。
performUnitOfWork
方法会创建下一个Fiber节点并赋值给workInProgress,并将workInProgress与已创建的Fiber节点连接起来构成Fiber树。
而源码实现中所谓的“递归”阶段就体现在performUnitOfWork
方法中,也就是beginWork
和completeWork
。
这里的递归很好理解,可以参考另一篇博客中的讲解,已经比较清晰了,实际上就是深度优先遍历。
考虑到性能优化,文本叶子节点不参与遍历。
2. beginWork
一句话总结,beginWork的工作是传入当前Fiber节点,创建子Fiber节点。也就是构建wip fiber tree。
mount时会直接创建,update时会根据diff算法决定是否复用再创建。具体看2.3小节
先看看传入的参数:
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
// ...省略函数体
}
- current:当前组件对应的Fiber节点在上一次更新时的Fiber节点,即workInProgress.alternate,也就是current tree上的节点。
- workInProgress:当前组件对应的Fiber节点
- renderLanes:优先级(Scheduler)相关
我们可以通过current是否等于null来确定当前是mount阶段还是update阶段。因为mount阶段是没有current tree的。
因此我们就可以根据这个判断条件,来区分开beginWork
在mount和update时的不同内容:
2.1 mount
除fiberRootNode
以外,current === null。会根据fiber.tag
不同,创建不同类型的子Fiber节点。
所以mount时,代码长这样:
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes
): Fiber | null {
// mount时:根据tag不同,创建不同的子Fiber节点
switch (workInProgress.tag) {
case IndeterminateComponent:
// ...省略
case LazyComponent:
// ...省略
case FunctionComponent:
// ...省略
case ClassComponent:
// ...省略
case HostRoot:
// ...省略
case HostComponent:
// ...省略
case HostText:
// ...省略
// ...省略其他类型
}
}
2.2 update
如果current存在,在满足一定条件时可以复用current节点,这样就能克隆current.child作为workInProgress.child,而不需要新建workInProgress.child。
这里也就用到了diff算法来决定复不复用。
满足以下两种情况时,wip tree直接复用current tree的节点(didReceiveUpdate === false
):
- oldProps === newProps && workInProgress.type === current.type,即props与fiber.type不变。
- !includesSomeLane(renderLanes, updateLanes),即当前Fiber节点优先级不够。
代码长这样:
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) {
// 省略处理
}
// 复用current
return bailoutOnAlreadyFinishedWork(
current,
workInProgress,
renderLanes,
);
} else {
didReceiveUpdate = false;
}
} else {
didReceiveUpdate = false;
}
2.3 reconcileChildren
上面提到新建子节点的时候会根据不同的tag来决定调用什么方法,对于我们常见的组件类型,如FunctionComponent/ClassComponent/HostComponent
,最终就会进入这个reconcileChildren
方法。
-
对于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,
);
}
}
不论走哪个逻辑,最终他会生成新的子Fiber节点并赋值给workInProgress.child
,作为本次beginWork
返回值,并作为下次performUnitOfWork
执行时workInProgress
的传参。
2.4 总结
最终来看看beginWork的工作流程图:
3. completeWork
组件执行beginWork
后会创建子Fiber节点,节点上可能存在effectTag(update的情况下)。那completeWork
会做什么工作呢?
先看看代码:
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;
}
// ...省略
首先入参和beginWork
是差不多的,就不展开说明;其次,可以看出来completeWork
也是根据不同fiber.tag
调用不同的处理逻辑。
3.1 HostComponent
先来看看页面渲染所必须的HostComponent
HostComponent指的是对应于DOM元素或者原生组件的React组件,比如div。
同样的,也得根据current是否为null来判断当前是mount阶段还是update阶段。
3.1.1 mount
mount时的主要逻辑包括三个:
- 为Fiber节点生成对应的DOM节点
- 将子孙DOM节点插入刚生成的DOM节点中
- 与update逻辑中的updateHostComponent类似的处理props的过程
注意,这里只是将DOM节点创建出来放在内存中,commit阶段才会将这个DOM节点插入到真正的DOM树中。
// ...省略服务端渲染相关逻辑
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);
}
mount时只会在rootFiber存在Placement effectTag。那么commit阶段是如何通过一次插入DOM操作(对应一个Placement effectTag)将整棵DOM树插入页面的呢?
原因就在于上面代码的appendAllChildren方法。
由于completeWork属于“归”阶段调用的函数,每次调用appendAllChildren时都会将已生成的子孙DOM节点插入当前生成的DOM节点下。那么当“归”到rootFiber时,我们已经有一个构建好的离屏DOM树,直接整棵树插入页面即可。
3.1.2 update
当update时,Fiber节点已经存在对应DOM节点,所以不需要生成DOM节点。需要做的主要是处理props,比如:
- onClick、onChange等回调函数的注册
- 处理style prop
- 处理DANGEROUSLY_SET_INNER_HTML prop
- 处理children prop
最主要的逻辑是调用updateHostComponent
方法。
if (current !== null && workInProgress.stateNode != null) {
// update的情况
updateHostComponent(
current,
workInProgress,
type,
newProps,
rootContainerInstance,
);
}
在updateHostComponent
内部,被处理完的props会被赋值给workInProgress.updateQueue
,并最终会在commit阶段被渲染在页面上。
3.2 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
为起点的单向链表。
这样,在commit阶段只需要遍历effectList
就能执行所有effect了。
借用React团队成员Dan Abramov的话:effectList相较于Fiber树,就像圣诞树上挂的那一串彩灯。
3.3 总结
completeWork
工作流程图
至此,render阶段全部工作完成。在performSyncWorkOnRoot
或是performConcurrentWorkOnRoot
函数中,fiberRootNode
被传递给commitRoot
方法,开启commit
阶段工作流程。