1、completeWork主要逻辑
completeWork的工作就是将Fiber节点映射为 DOM 节点;
performUnitOfWork 到 completeWork,中间会经过一个这样的调用链路:
performUnitOfWork源码:
function performUnitOfWork(unitOfWork) {
......
// 获取入参节点对应的 current 节点
var current = unitOfWork.alternate;
var next;
if (xxx) {
...
// 创建当前节点的子节点
next = beginWork$1(current, unitOfWork, subtreeRenderLanes);
...
} else {
// 创建当前节点的子节点
next = beginWork$1(current, unitOfWork, subtreeRenderLanes);
}
......
if (next === null) {
// 调用 completeUnitOfWork
completeUnitOfWork(unitOfWork);
} else {
// 将当前节点更新为新创建出的 Fiber 节点
workInProgress = next;
}
......
}
performUnitOfWork 每次会尝试调用 beginWork 来创建当前节点的子节点,若创建出的子节点为空(也就意味着当前节点不存在子 Fiber 节点),则说明当前节点是一个叶子节点。按照深度优先遍历的原则,当遍历到叶子节点时,“递”阶段就结束了,随之而来的是“归”的过程。因此这种情况下,就会调用 completeUnitOfWork,执行当前节点对应的 completeWork 逻辑。
2、completeWork 的工作原理
completeWork源码:
function completeWork(current, workInProgress, renderLanes) {
// 取出 Fiber 节点的属性值,存储在 newProps 里
var newProps = workInProgress.pendingProps;
// 根据 workInProgress 节点的 tag 属性的不同,决定要进入哪段逻辑
switch (workInProgress.tag) {
case ......:
return null;
case ClassComponent:
{
.....
}
case HostRoot:
{
......
}
case HostComponent:
{
popHostContext(workInProgress);
var rootContainerInstance = getRootHostContainer();
var type = workInProgress.type;
// 判断 current 节点是否存在,因为目前是挂载阶段,因此 current 节点是不存在的
if (current !== null && workInProgress.stateNode != null) {
updateHostComponent$1(current, workInProgress, type, newProps, rootContainerInstance);
if (current.ref !== workInProgress.ref) {
markRef$1(workInProgress);
}
} else {
// 这里首先是针对异常情况进行 return 处理
if (!newProps) {
if (!(workInProgress.stateNode !== null)) {
{
throw Error("We must have new props for new mounts. This error is likely caused by a bug in React. Please file an issue.");
}
}
return null;
}
// 接下来就为 DOM 节点的创建做准备了
var currentHostContext = getHostContext();
// _wasHydrated 是一个与服务端渲染有关的值,这里不用关注
var _wasHydrated = popHydrationState(workInProgress);
// 判断是否是服务端渲染
if (_wasHydrated) {
// 这里不用关注,请你关注 else 里面的逻辑
if (prepareToHydrateHostInstance(workInProgress, rootContainerInstance, currentHostContext)) {
markUpdate(workInProgress);
}
} else {
// 这一步很关键, createInstance 的作用是创建 DOM 节点
var instance = createInstance(type, newProps, rootContainerInstance, currentHostContext, workInProgress);
// appendAllChildren 会尝试把上一步创建好的 DOM 节点挂载到 DOM 树上去
appendAllChildren(instance, workInProgress, false, false);
// stateNode 用于存储当前 Fiber 节点对应的 DOM 节点
workInProgress.stateNode = instance;
// finalizeInitialChildren 用来为 DOM 节点设置属性
if (finalizeInitialChildren(instance, type, newProps, rootContainerInstance)) {
markUpdate(workInProgress);
}
}
......
}
return null;
}
case HostText:
{
......
}
case SuspenseComponent:
{
......
}
case HostPortal:
......
return null;
case ContextProvider:
......
return null;
......
}
{
{
throw Error("Unknown unit of work tag (" + workInProgress.tag + "). This error is likely caused by a bug in React. Please file an issue.");
}
}
}
- completeWork 的核心逻辑是一段体量巨大的 switch 语句,在这段 switch 语句中,completeWork 将根据 workInProgress 节点的 tag 属性的不同,进入不同的 DOM 节点的创建、处理逻辑。
- 本次源码的重点是看节点的h1的tag属性对应的类型应该是 HostComponent,也就是“原生 DOM 元素类型”。
- completeWork 中的 current、 workInProgress 分别对应的是下图中左右两棵 Fiber 树上的节点:
其中 workInProgress 树代表的是“当前正在 render 中的树”,而 current 树则代表“已经存在的树”。
workInProgress 节点和 current 节点之间用 alternate 属性相互连接。在组件的挂载阶段,current 树只有一个 rootFiber 节点,并没有其他内容。因此 h1 这个 workInProgress 节点对应的 current 节点是 null。
completeWork的主要工作:
- 负责处理 Fiber 节点到 DOM 节点的映射逻辑。
- completeWork 内部有 3 个关键动作:
- 创建DOM 节点(CreateInstance)
- 将 DOM 节点插入到 DOM 树中(AppendAllChildren)
- 为 DOM 节点设置属性(FinalizeInitialChildren)
- 创建好的 DOM 节点会被赋值给 workInProgress 节点的 stateNode 属性
- 将 DOM 节点插入到 DOM 树的操作是通过 appendAllChildren 函数来完成的,实际上是将子 Fiber 节点所对应的 DOM 节点挂载到其父 Fiber 节点所对应的 DOM 节点里去。
3、completeUnitOfWork工作原理
completeUnitOfWork开启收集 EffectList 的“大循环”;
- 针对传入的当前节点,调用 completeWork;
- 将当前节点的副作用链(EffectList)插入到其父节点对应的副作用链(EffectList)中;
- 以当前节点为起点,循环遍历其兄弟节点及其父节点。当遍历到兄弟节点时,将 return 掉当前调用,触发兄弟节点对应的 performUnitOfWork 逻辑;而遍历到父节点时,则会直接进入下一轮循环,也就是重复 1、2 的逻辑。
理解副作用链之前,首先要理解 completeUnitOfWork 开启下一轮循环的原则,也就是步骤 3。步骤 3 相关的源码如下所示:
do {
......
// 这里省略步骤 1 和步骤 2 的逻辑
// 获取当前节点的兄弟节点
var siblingFiber = completedWork.sibling;
// 若兄弟节点存在
if (siblingFiber !== null) {
// 将 workInProgress 赋值为当前节点的兄弟节点
workInProgress = siblingFiber;
// 将正在进行的 completeUnitOfWork 逻辑 return 掉
return;
}
// 若兄弟节点不存在,completeWork 会被赋值为 returnFiber,也就是当前节点的父节点
completedWork = returnFiber;
// 这一步与上一步是相辅相成的,上下文中要求 workInProgress 与 completedWork 保持一致
workInProgress = completedWork;
} while (completedWork !== null);
步骤 3 是整个循环体的收尾工作,它会在当前节点相关的各种工作都做完之后执行。
当前的 Fiber 节点之所以会进入 completeWork,是因为“递无可递”了,才会进入“归”的逻辑,这就意味着当前 Fiber 要么没有 child 节点、要么 child 节点的 completeWork 早就执行过了。因此 child 节点不会是下次循环需要考虑的对象,下次循环只需要考虑兄弟节点(siblingFiber)和父节点(returnFiber)。
4、render阶段的做工作目标
render 阶段的工作目标是找出界面中需要处理的更新。
更新阶段与挂载阶段的主要区别在于更新阶段的 current 树不为空;
假如说我的某一次操作,仅仅对 p 节点产生了影响,那么对于渲染器来说,它理应只关注 p 节点这一处的更新。这时候问题就来了:怎样做才能让渲染器又快又好地定位到那些真正需要更新的节点呢?
答案是:副作用链(effectList)
副作用链(effectList) 可以理解为 render 阶段“工作成果”的一个集合:每个 Fiber 节点都维护着一个属于它自己的 effectList,effectList 在数据结构上以链表的形式存在,链表内的每一个元素都是一个 Fiber 节点。这些 Fiber 节点需要满足两个共性:
- 都是当前 Fiber 节点的后代节点
- 都有待处理的副作用
Fiber 节点的 effectList 里记录的并非它自身的更新,而是其需要更新的后代节点。
“completeWork 是自底向上执行的”,子节点的 completeWork 总是比父节点先执行。
把所有需要更新的 Fiber 节点单独串成一串链表,方便后续有针对性地对它们进行更新,这就是所谓的“收集副作用”的过程。
nextEffect nextEffect
rootFiber.firstEffect -----------> fiber -----------> fiber
总结一下effectList 的创建过程:
- App FiberNode 的 flags 属性为 3,大于 PerformedWork,因此会进入 effectList 的创建逻辑;
- 创建 effectList 时,并不是为当前 Fiber 节点创建,而是为它的父节点创建,App 节点的父节点是 rootFiber,rootFiber 的 effectList 此时为空;
- rootFiber 的 firstEffect 和 lastEffect 指针都会指向 App 节点,App 节点由此成为 effectList 中的唯一一个 FiberNode,如下图所示。