阅读 247

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

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 阶段只会执行一次插入操作。


作者:试试是是
链接:https://juejin.cn/post/7036194010565705764

 伪原创工具 SEO网站优化  https://www.237it.com/ 


文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐