1
头图

本文对应的 react 版本是 18.2.0

通过上两讲:

  1. 掌握 React 组件树遍历技巧
  2. useEffect 返回的函数是怎么执行的

我们已经知道了 react 是如何找到 passive effect 返回的函数

那么找到这个函数后,怎么执行这个函数呢

我们先来看下面这段代码:

function A() {
  useEffect(() => {
    return () => {
      console.log("执行销毁函数 A");
    };
  }, []);
  useEffect(() => {
    return () => {
      console.log("执行销毁函数 A1");
    };
  }, []);
  return <>文本A</>;
}

一个组件中有两个 passive effect 返回的函数,react 是怎么安排执行的顺序呢?

一个组件中的 passive effect 是用链表的形式存储的

每个 effect 对象都有 destroynext 属性

  • destroy 保存的是 passive effect 返回的函数
  • next 保存的是下一个 effect 对象

最顶层的 effect 是函数组件中写在最上面的 useEffect,通过 next 指向下一个 effect,以此类推,最后一个 effectnext 指向最顶层的 effect

结构如下所示:

let effect = {
  destroy: () => {
    console.log("执行销毁函数 A"));
  },
  next: {
    destroy: () => {
      console.log("执行销毁函数 A1");
    },
    next: {
      destroy: () => {
        console.log("执行销毁函数 A");
      },
      next: { ... },
    },
  },
};

既然是链表,那么执行的顺序就是从最顶层的 effect 开始,依次执行 destroy 函数,最后执行最顶层的 effectdestroy 函数

源码简化:

function commitHookEffectListUnmount(
  flags: HookFlags,
  finishedWork: Fiber,
  nearestMountedAncestor: Fiber | null
) {
  const updateQueue: FunctionComponentUpdateQueue | null =
    (finishedWork.updateQueue: any);
  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      if ((effect.tag & flags) === flags) {
        // Unmount
        const destroy = effect.destroy;
        effect.destroy = undefined;
        if (destroy !== undefined) {
          destroy();
        }
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

react 这里使用 do...while 进行遍历,保证所有的 effect 都被执行

释放内存

释放内存分为两个阶段:

  1. 第一个阶段是在向上遍历时
  2. 第二个阶段是在处理完成 deletions

detachFiberAfterEffects

上回说到 react 在处理 deletedNode 时先向下遍历,然后在向上遍历

在向上遍历的过程中会将对应所有遍历到的 fiber 的属性都置为 null,这样可以释放一些内存

function detachFiberAfterEffects(fiber) {
  const alternate = fiber.alternate;
  if (alternate !== null) {
    fiber.alternate = null;
    detachFiberAfterEffects(alternate);
  }
  fiber.child = null;
  fiber.deletions = null;
  fiber.sibling = null;

  if (fiber.tag === HostComponent) {
    const hostInstance = fiber.stateNode;
    if (hostInstance !== null) {
      delete hostInstance[internalInstanceKey];
      delete hostInstance[internalPropsKey];
      delete hostInstance[internalEventHandlersKey];
      delete hostInstance[internalEventHandlerListenersKey];
      delete hostInstance[internalEventHandlesSetKey];
    }
  }
  fiber.stateNode = null;
  fiber.return = null;
  fiber.dependencies = null;
  fiber.memoizedProps = null;
  fiber.memoizedState = null;
  fiber.pendingProps = null;
  fiber.stateNode = null;
  fiber.updateQueue = null;
}

detachAlternateSiblings

当处理完 deletions 时,当前 fiberalternatealternate 下所有的子节点也会被置为 null,这样可以释放一些内存

function detachAlternateSiblings(parentFiber) {
  const previousFiber = parentFiber.alternate;
  if (previousFiber !== null) {
    let detachedChild = previousFiber.child;
    if (detachedChild !== null) {
      previousFiber.child = null;
      do {
        const detachedSibling = detachedChild.sibling;
        detachedChild.sibling = null;
        detachedChild = detachedSibling;
      } while (detachedChild !== null);
    }
  }
}

根节点处理

react 每次遍历都是从根节点开始,那么根节点的处理是怎么样的呢?

在这里 掌握 React 组件树遍历技巧 我们知道 react 是通过调用 commitPassiveUnmountOnFiber 函数来寻找有 passive effectfiber

按照源码去追踪,我们会发现在 recursivelyTraversePassiveUnmountEffects 函数中会调用 commitHookPassiveUnmountEffects 函数,具体解释可以查这里:commitPassiveUnmountOnFiber

源码简化:

function commitPassiveUnmountOnFiber(finishedWork, type) {
  recursivelyTraversePassiveUnmountEffects(finishedWork);
  if (finishedWork.flags & Passive) {
    commitHookPassiveUnmountEffects(
      finishedWork,
      finishedWork.return,
      HookPassive | HookHasEffect
    );
  }
}

react 为什么要多此一举呢?

通过不断的打断点会看到,commitHookPassiveUnmountEffects 函数会被调用两次

recursivelyTraversePassiveUnmountEffects 函数处理的是 finishedWork.chile,而 commitHookPassiveUnmountEffects 函数处理的是 finishedWork

因为 react 是从根节点开始遍历的,所以 commitHookPassiveUnmountEffects 只处理根节点的 passive effect 的返回函数

总结

  1. react 从根组件开始遍历,寻找 passive effectfiber
  2. 在遍历时,会检查每个 fiberdeletions

    • 如果有则暂停 passive effect 的遍历,先处理 deletions
    • 处理完 deletions 后,再继续遍历 passive effectfiber
  3. 在处理 deletions 时,会先向下遍历,然后再向上遍历

    • 向下遍历时,执行 passive effect 的返回函数
    • 向上遍历时

      • 如果遇到 sibling,则会沿着 sibling 向下遍历
      • fiber 的所有属性置为 null,释放内存
      • 直到遇到 deletedNode 结束处理 deletions
  4. 根节点的 passive effect 返回的函数会单独处理

往期文章

  1. 深入探究 React 原生事件的工作原理
  2. React Lane 算法:一文详解 8 种 Lane 操作
  3. 剖析 React 任务调度机制:scheduleCallback 实现原理
  4. 掌握 React 组件树遍历技巧
  5. useEffect 返回的函数是怎么执行的

更多 react 源码文章


uccs
756 声望88 粉丝

3年 gis 开发,wx:ttxbg210604