递归阶段completeWork
在流程概览一节我们了解组件在render阶段
会经历beginWork
与completeWork
。
上一节我们讲解了组件执行beginWork
后会创建子Fiber节点
,节点上可能存在effectTag
。
这一节让我们看看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;
}
// ...省略
我们重点关注页面渲染所必须的HostComponent
(即原生DOM组件
对应的Fiber节点
),其他类型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;
}
update时
当update
时,Fiber节点
已经存在对应DOM节点
,所以不需要生成DOM节点
。需要做的主要是处理props
,比如:
onClick
、onChange
等回调函数的注册- 处理
style prop
- 处理
DANGEROUSLY_SET_INNER_HTML prop
- 处理
children prop
我们去掉一些当前不需要关注的功能(比如ref
)。可以看到最主要的逻辑是调用updateHostComponent
方法。
if (current !== null && workInProgress.stateNode != null) {
// update的情况
updateHostComponent(
current,
workInProgress,
type,
newProps,
rootContainerInstance,
);
}
你可以从这里看到updateHostComponent
方法定义。
在updateHostComponent
内部,被处理完的props
会被赋值给workInProgress.updateQueue
,并最终会在commit阶段
被渲染在页面上。
workInProgress.updateQueue = (updatePayload: any);
其中updatePayload
为数组形式,他的偶数索引的值为变化的prop key
,奇数索引的值为变化的prop value
。
具体渲染过程见mutation阶段一节
::: details updatePayload属性 demo
updateHostComponent
方法内打印了Fiber节点
对应的type
与updatePayload
。
你可以直观的感受updatePayload
的数据结构
:::
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);
}
还记得上一节我们讲到: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
这样,在commit阶段
只需要遍历effectList
就能执行所有effect
了。
你可以在这里看到这段代码逻辑。
借用React
团队成员Dan Abramov的话:effectList
相较于Fiber树
,就像圣诞树上挂的那一串彩灯。
流程结尾
至此,render阶段
全部工作完成。在performSyncWorkOnRoot
函数中fiberRootNode
被传递给commitRoot
方法,开启commit阶段
工作流程。
commitRoot(root);
代码见这里。
参考资料
completeWork
流程图