React EffectList

1,742 阅读5分钟

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