乐趣区

关于javascript:组件卸载时-DOM-树的自动清理机制是怎样的

本文对应的 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 源码文章

退出移动版