DOM DIFF - 多节点

30 阅读3分钟

目标

要了解多节点 DIFF,首先需要知道已知条件和实现目标。

已知条件:

  • 老 fiber
  • 新的虚拟 DOM 节点数组

目标:根据新的虚拟 DOM 生成的新的 fiber 链。

截屏2025-07-28 21.14.03.png

第一轮遍历

对于前三种情况,只需要同时遍历新老节点,判断每个对应节点是否可以复用,如果可以复用,则复用并继续遍历下一个节点,如果不能复用则退出当前遍历。

在这次遍历结束后,判断新节点是否遍历完,如果新节点遍历完,则删除剩下的所有老节点。

如果老节点已遍历完成而新节点未遍历完成,则插入所有剩下的新节点。

第二轮遍历

在第四幅图中,为了尽可能复用老节点,需要移动老节点。

截屏2025-07-28 22.50.18.png

为了方便理解,上图中节点一一对应,只是打乱了顺序。为了确定老 fiber 节点是否需要移动,只需要确定老 fiber 中的相对顺序符合新 fiber 中的相对顺序,如上图中 B、C 节点,新老节点中都是 B 节点在 C 节点前,所有 B、C 节点不需要移动。

可以引入一个变量,标记上一个不需要移动的老 fiber 节点的下标。当遍历新节点时,找到对应的老 fiber,并获取其下标,如果其下标大于记录的上一个不需要移动的老节点下标,则更新不需要移动的老 fiber 节点的下标变量。如果小于,则更新 fiber 链表,并继续遍历。

遍历结束后,会得到一个根据新的虚拟 DOM 生成的新 fiber 链表,其顺序和新虚拟 DOM 顺序一致。最后返回该虚拟 DOM。

对应代码:

function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren) {
    // 返回的第一个新儿子
    let resultingFirstChild = null;
    // 上一个新 fiber
    let previousNewFiber = null;
    let newIdx = 0; // 用来遍历新的虚拟 DOM 的索引
    let oldFiber = currentFirstChild; // 第一个老 fiber
    let nextOldFiber = null; // 下一个老 fiber
    let lastPlacedIndex = 0; // 上一个不需要移动的老 fiber 的索引

    // 开始第一轮循环,如果老 fiber 有值,新的 fiber 也有值
    for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
      // 暂存下一个老 fiber
      nextOldFiber = oldFiber.sibling;
      // 试图复用老 fiber
      const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx]);
      if (!newFiber) {
        break;
      }
      if (shouldTrackSideEffects) {
        // 如果有老 fiber,但是新 fiber 并没有成功复用老 fiber 和老的真实 DOM,那就删除老 fiber
        // 在提交阶段删除真实 DOM
        if (oldFiber && newFiber.alternate === null) {
          deleteChild(returnFiber, oldFiber);
        }
      }

      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);

      if (previousNewFiber === null) {
        resultingFirstChild = newFiber;
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
      oldFiber = nextOldFiber;
    }

    // 如果新的 fiber 已经遍历完,则删除剩余的 oldFiber
    if (newIdx === newChildren.length) {
      deleteRemainingChildren(returnFiber, oldFiber);
    }

    // 如果老的 fiber 遍历完成,新 fiber 还没,则开始插入逻辑
    if (oldFiber === null) {
      for (; newIdx < newChildren.length; newIdx++) {
        const newFiber = createChild(returnFiber, newChildren[newIdx]);
        if (newFiber === null) {
          continue;
        }
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        if (previousNewFiber === null) {
          // 如果previousNewFiber 为 null,则说明这是第一个子 fiber
          resultingFirstChild = newFiber;
        } else {
          previousNewFiber.sibling = newFiber;
        }
        previousNewFiber = newFiber;
      }
    }

    // 开始处理移动的情况
    const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
    // 开始遍历剩下的虚拟 DOM 子节点
    for (; newIdx < newChildren.length; newIdx++) {
      const newFiber = updateFromMap(existingChildren, returnFiber, newIdx, newChildren[newIdx]);
      if (newFiber !== null) {
        if (shouldTrackSideEffects) {
          if (newFiber.alternate !== null) {
            existingChildren.delete(newFiber.key || newIdx);
          }
        }
      }
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      if (previousNewFiber === null) {
        // 如果previousNewFiber 为 null,则说明这是第一个子 fiber
        resultingFirstChild = newFiber;
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
    }

    if (shouldTrackSideEffects) {
      existingChildren.forEach((child) => deleteChild(returnFiber, child));
    }

    return resultingFirstChild;
  }