React EffectList
什么是 EffectList
一个由 Fiber 构成的单向链表。
每个 Fiber 节点都保存着自己子节点的 EffectList,Fiber 对象上有三个指针:firstEffct、lastEffect、nextEffect,分别指向下一个待处理的 effect fiber,第一个和最后一个待处理的 effect fiber。
为什么要有 EffectList
作为 DOM 操作的依据,commit 阶段需要找到所有有 effectTag 的 Fiber 节点并依次执行 effectTag 对应操作。难道需要在 commit 阶段再遍历一次 Fiber 树寻找 effectTag !== null 的 Fiber 节点么?
这显然是很低效的。
而 EffectList 就解决了这个问题,在 Fiber 树构建过程中,每当一个 Fiber 节点的 effectTag 字段不为 NoEffect 时(代表需要执行副作用),就把该 Fiber 节点添加到 EffectList,在 Fiber 树构建完成后,Fiber 树的 Effect List 也就构建完成
EffectList 的收集
在 completeWork 的上层函数 completeUnitOfWork 中,每个执行完 completeWork 且存在 effectTag 的 Fiber 节点会被保存在一条被称为 effectList 的单向链表中。effectList 中第一个 Fiber 节点保存在 fiber.firstEffect,最后一个元素保存在 fiber.lastEffect。
Fiber 树的构建是深度优先的,也就是先向下构建子级 Fiber 节点,子级节点构建完成后,再向上构建父级 Fiber 节点,所以 EffectList 中总是子级 Fiber 节点在前面。
completeUnitOfWork 函数中所做的工作:
-
完成该 fiber 节点的构建
-
将该 fiber 的 effectList 更新到其父 Fiber 节点上
-
如果当前节点有 effectTag,则将其加入 effectList
-
如果有 sibling,移动到 next sibling 进行同样的操作
-
没有 sibling 则返回父 fiber
function completeUnitOfWork(unitOfWork: Fiber): void {
let completedWork = unitOfWork;
do {
const current = completedWork.alternate;
const returnFiber = completedWork.return;
let next = completeWork(current, completedWork, subtreeRenderLanes);
// effect list构建
if (
returnFiber !== null &&
// Do not append effects to parents if a sibling failed to complete
(returnFiber.effectTag & Incomplete) === NoEffect
) {
// Append all the effects of the subtree and this fiber onto the effect
// list of the parent. The completion order of the children affects the
// side-effect order.
if (returnFiber.firstEffect === null) {
returnFiber.firstEffect = completedWork.firstEffect;
}
if (completedWork.lastEffect !== null) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
}
returnFiber.lastEffect = completedWork.lastEffect;
}
const effectTag = completedWork.effectTag;
if (effectTag > PerformedWork) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork;
} else {
returnFiber.firstEffect = completedWork;
}
returnFiber.lastEffect = completedWork;
}
}
// 兄弟元素遍历再到返返回父级
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
workInProgress = siblingFiber;
return;
}
completedWork = returnFiber;
workInProgress = completedWork;
} while (completedWork !== null);
}
看一个例子
<div id="1">
<div id="4" />
<div id="2">
<div id="3" />
</div>
</div>
最终形成的 EffectList 为
firstEffect => div4
lastEffect => div1
因为 Fiber 树的构建深度优先,所以 div4 先完成 completeWork,构建 firstEffect。
EffectList 遍历是从 firstEffect 开始,通过每一个节点的 nextEffect 找到下一个节点。
firstEffect => div4
div4.nextEffect => div3
div3.nextEffect => div2
div2.nextEffect => div1
所以最终形成一条以 rootFiber.firstEffect 为起点的单向链表。
这样,在 commit 阶段只需要遍历 effectList 就能执行所有 effect 了。
nextEffect nextEffect
rootFiber.firstEffect -----------> fiber -----------> fiber
EffectList 的遍历
commit 阶段就会从 rootFiber.firstEffect 开始遍历这个 effectList 来执行副作用
总结
在 beginWork 中我们知道有的节点被打上了 effectTag 的标记,有的没有,而在 commit 阶段时要遍历所有包含 effectTag 的 Fiber 来执行对应的增删改,那我们还需要从 Fiber 树中找到这些带 effectTag 的节点嘛,答案是不需要的,这里是以空间换时间,在执行 completeUnitOfWork 的时候遇到了带 effectTag 的节点,会将这个节点加入一个叫 effectList 中,所以在 commit 阶段只要遍历 effectList 就可以了(rootFiber.firstEffect.nextEffect 就可以访问带 effectTag 的 Fiber 了)每个 fiber 节点上都保存了该 fiber 节点的子节点的 effectList,通过 firstEffect、nextEffect、LastEffect 来保存,在 completeWork 的时候就会将每个 fiber 的 effectList 更新到其父 Fiber 节点上,所以 complete 之后,rootFiber 上就保存了完整的 effectList,我们在 commit 阶段就直接遍历 rootFiber 上的 effectList 来执行副作用即可
EffectList 不是全局变量,只是在 Fiber 树创建过程中,一层层向上收集有 effect 的 Fiber 节点,最终的 root 节点就会收集到所有有 effect 到 Fiber 节点,我们就把这条包含 effect 节点的链表叫做 EffectList。
由于收集的过程是深度优先,子级会先被收集,所以遍历的时候也会先操作子级,所以如果有面试官问子级和父级的生命周期或者 useEffect 谁先执行,就很清楚的知道会先执行子级操作了。
补充
effectTag
当 reconciler 工作结束后会通知 Renderer 需要执行的 DOM 操作。要执行 DOM 操作的具体类型就保存在 fiber.effectTag 中。
// DOM需要插入到页面中
export const Placement = /* */ 0b00000000000010;
// DOM需要更新
export const Update = /* */ 0b00000000000100;
// DOM需要插入到页面中并更新
export const PlacementAndUpdate = /* */ 0b00000000000110;
// DOM需要删除
export const Deletion = /* */ 0b00000000001000;
初次 Render 时的 EffectList
在 React 中,会对初次 Mount 有一个性能优化,其中的 Fiber 节点的 effectTag 不会包含 placement,对应的 DOM 节点不会遍历加入 DOM 树,而是在创建 DOM 节点时就已经加入 DOM 树了,只有 rootFiber 节点 FiberRootNode 的 effectTag 会包含 placement。
EffectList 是不会包含 root 节点的,所以需要将 root 节点也添加到 EffectList,这样才会正确的执行 placement,让 DOM 树在页面呈现 。
let firstEffect;
// 把根节点finishedWork也连接进去
if (finishedWork.effectTag > PerformedWork) {
if (finishedWork.lastEffect !== null) {
finishedWork.lastEffect.nextEffect = finishedWork;
firstEffect = finishedWork.firstEffect;
} else {
firstEffect = finishedWork;
}
} else {
// 根节点没有effect.
firstEffect = finishedWork.firstEffect;
}
那么,如果要通知 Renderer 将 Fiber 节点对应的 DOM 节点插入页面中,需要满足两个条件:
-
fiber.stateNode 存在,即 Fiber 节点中保存了对应的 DOM 节点
-
(fiber.effectTag & Placement) !== 0,即 Fiber 节点存在 Placement effectTag
我们知道,mount 时,fiber.stateNode === null
,且在reconcileChildren
中调用的mountChildFibers
不会为 Fiber 节点赋值 effectTag。那么首屏渲染如何完成呢?
针对第一个问题,fiber.stateNode 会在 completeWork 中创建。
第二个问题的答案十分巧妙:假设 mountChildFibers 也会赋值 effectTag,那么可以预见 mount 时整棵 Fiber 树所有节点都会有 Placement effectTag。那么 commit 阶段在执行 DOM 操作时每个节点都会执行一次插入操作,这样大量的 DOM 操作是极低效的。
为了解决这个问题,在 mount 时只有 rootFiber 会赋值 Placement effectTag,在 commit 阶段只会执行一次插入操作。
网友评论