react原理专栏
这篇文章讲一下render阶段的第二部分:completeUnitOfWork。
react的render阶段,会按照先序遍历的顺序构建fiber树,当workInProgress指针来到一个叶子节点时,就会执行completeUnitOfWork方法,这个方法会修改workInProgress指针,让其指向先序遍历中的下一个节点,下面,就来介绍一下completeUnitOfWork完成的工作。
completeUnitOfWork主要做了三件事:dom节点的创建和更新,effectList的收集和错误的处理。关于错误处理的内容,笔者也没有深入了解过,这篇文章就介绍一下前两部分内容:dom节点的创建和更新。
Dom节点的创建和更新
这部分的内容在completeWork函数中。completeWork会根据不同的组件类型进入不同的逻辑。由于completeWork主要会做dom相关的操作,因此我们可以主要看HostComponent部分的代码。
popHostContext(workInProgress);
var rootContainerInstance = getRootHostContainer();
var type = workInProgress.type;
if (current !== null && workInProgress.stateNode != null) {
// 节点更新
updateHostComponent$1(current, workInProgress, type, newProps, rootContainerInstance);
if (current.ref !== workInProgress.ref) {
markRef$1(workInProgress);
}
} else {
// 新增dom节点
// ...
var currentHostContext = getHostContext();
var _wasHydrated = popHydrationState(workInProgress);
if (_wasHydrated) {
// 服务端渲染相关
} else {
var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);
appendAllChildren(instance, workInProgress, false, false);
workInProgress.stateNode = instance;
if (finalizeInitialChildren(instance, type, newProps, rootContainerInstance)) {
markUpdate(workInProgress);
}
}
if (workInProgress.ref !== null) {
markRef$1(workInProgress);
}
}
return null;
可以看到,对于HostComponent的处理分为dom更新和dom创建两个过程。针对更新,主要是dom属性的更新,对于新增的dom节点,会先创建一个dom节点,之后将该节点挂载到fiber的stateNode属性上,之后插入该节点(appendAllChildren)。由于在fiber架构中,组件树和dom并不是对应的,比如函数组件和类组件不存在对应的dom节点,因此appendAllChildren这部分的逻辑比较繁琐,这里就不展开讲解了,有兴趣的可以看一下@Axizs大佬的文章。
effectList收集
在完成dom相关的更新之后,回到completeUnitOfWork中,会开始effectList链的收集。effectList链的作用就是将有副作用的fiber节点收集为一条链表,这样在commit阶段中,就可以根据effectList来进行操作,不需要再遍历一次fiber树。下面看一下这部分代码
// returnFiber就是当前正在处理的workInProgress节点的父节点
// completedWork就是正在处理的fiber节点
if (returnFiber.firstEffect === null) {
returnFiber.firstEffect = completedWork.firstEffect;
}
// 将completedWork身上的effectList拼接到returnFiber的后面
if (completedWork.lastEffect !== null) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
}
returnFiber.lastEffect = completedWork.lastEffect;
}
var flags = completedWork.flags;
// 将completedWork自己拼接到returnFiber的后面
if (flags > PerformedWork) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork;
} else {
returnFiber.firstEffect = completedWork;
}
returnFiber.lastEffect = completedWork;
}
也就是说,effectList的顺序是从层次最深的子节点开始向上收集,层次最深的fiber节点位于effectList链表的头部。最终,workInProgress树的rootFiber节点会得到完整的effectList。不是很清楚的同学可以看一下b站的这个视频。
注意点
关于completework部分有一些需要注意的地方:
completeWork中创建了新的dom节点,但是此时,这些新的dom节点并没有被插入到dom树中,比如看下面的例子
import { useState, useCallback } from 'react'
const App = () => {
const [visible, change] = useState(false)
const changeVisible = useCallback(() => change((pre) => !pre), [change])
return (
<div>
{visible && (
<div>
<h1>新的div</h1>
</div>
)}
<p>老的内容</p>
<button onClick={changeVisible}>click</button>
</div>
)
}
export default App
点击按钮,visible变为true,此时,在completework中,h1被添加到了div中,但是div并没有被添加到dom树中。相反,这个div会被打上placement的tag,从而被收集到effectList中,在之后的commit阶段中,这个div会被添加到真实的dom树中,从而实现页面的更新。
在completeWork之后,render阶段结束,进入commit阶段,之后的文章会为大家讲解。