美文网首页
React EffectList

React EffectList

作者: 前端小白的摸爬滚打 | 来源:发表于2021-11-30 10:58 被阅读0次

    React EffectList

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

    相关文章

      网友评论

          本文标题:React EffectList

          本文链接:https://www.haomeiwen.com/subject/ycfdxrtx.html