DOM DIFF - 单节点

64 阅读2分钟

什么是 DOM DIFF

DOM DIFF 指的是针对两个(通常是虚拟)DOM 树结构进行“差异比对”的算法。在 react 中,进行比较的则是 fiber 架构。其目标有两个:

  1. 比较前后两棵树的节点差异
  2. 生成最小化的增删改操作序列,以便高效地把真实浏览器 DOM 从旧状态“打补丁”更新到新状态(复用节点、移动节点、删除节点)

这里讨论的是新的 fiber 节点为单节点的情况。

如何比较

DOM DIFF 规则:

  1. 同级比较,不跨级比较
  2. key 为唯一索引
  3. 批量更新

数据结构:

截屏2025-07-27 22.23.42.png

上图为需要比较的新旧 fiber 结构,目标为找到相同的 fiber 结构进行复用。相同的条件为 key 相等,type 相等。

流程图:

截屏2025-07-27 22.32.34.png

根据这个流程图,结合之前的 beginWork 阶段,在触发更新重新渲染页面时,可以对比 DIFF 子节点。

  /**
   *
   * @param {*} returnFiber 父 fiber
   * @param {*} currentFirstChild 老 fiber
   * @param {*} element 新的虚拟 DOM
   */
  function reconcileSingleElement(returnFiber, currentFirstChild, element) {
    // 新虚拟 DOM 的 key
    const key = element.key;
    let child = currentFirstChild;

    while (child !== null) {
      // 判断老 fiber 和新虚拟 DOM 的 key 是否相等
      if (child.key === key) {
        // 判断老 fiber 和新虚拟 DOM 的类型是否相等
        if (child.type === element.type) {
          deleteRemainingChildren(returnFiber, child.sibling);
          // 如果都一样,则可以复用
          const existing = useFiber(child, element.props);
          existing.return = returnFiber;
          return existing;
        } else {
          // 如果类型不同,则删除老的,创建新的
          deleteRemainingChildren(returnFiber, child);
        }
      } else {
        deleteChild(returnFiber, child);
      }
      child = child.sibling;
    }

    // 实现初次挂载
    const created = createFiberFromElement(element);
    created.return = returnFiber;
    return created;
  }
  
  
  function deleteChild(returnFiber, childToDelete) {
    if (!shouldTrackSideEffects) {
      return;
    }
    // 删除的 fiber
    const deletions = returnFiber.deletions;
    if (deletions === null) {
      returnFiber.deletions = [childToDelete];
      returnFiber.flags |= ChildDeletion;
    } else {
      deletions.push(childToDelete);
    }
  }

  function deleteRemainingChildren(returnFiber, currentFirstChild) {
    if (!shouldTrackSideEffects) {
      return;
    }
    let childToDelete = currentFirstChild;
    while (childToDelete !== null) {
      deleteChild(returnFiber, childToDelete);
      childToDelete = childToDelete.sibling;
    }
  }

在 deleteChild 中出现了一个字段 deletions,这是在当前父 fiber 节点中记录需要删除的子 fiber 节点,在提交阶段统一删除。

提交阶段代码:


/**
 * 提交删除副作用
 * @param {*} root 根节点
 * @param {*} returnFiber 父 fiber
 * @param {*} deletions 要删除的 fiber
 */
function commitDeletionEffects(root, returnFiber, deletedFiber) {
  let parent = returnFiber;
  // 找到真实 DOM 节点
  findParent: while (parent !== null) {
    switch (parent.tag) {
      case HostRoot:
        hostParent = parent.stateNode.containerInfo;
        break findParent;
      case HostComponent:
        hostParent = parent.stateNode;
        break findParent;
    }
    parent = parent.return;
  }
  commitDeletionEffectsOnFiber(root, returnFiber, deletedFiber);
  hostParent = null;
}

function commitDeletionEffectsOnFiber(finishedRoot, nestedMountedAncestor, deletedFiber) {
  switch (deletedFiber.tag) {
    case HostComponent:
    case HostText: {
      // 删除文本节点
      recursivelyTraverseDeletionEffects(finishedRoot, nestedMountedAncestor, deletedFiber);
      if (hostParent !== null) {
        removeChild(hostParent, deletedFiber.stateNode);
      }
      break;
    }
  }
}

function recursivelyTraverseDeletionEffects(finishedRoot, nestedMountedAncestor, parent) {
  let child = parent.child;
  while (child !== null) {
    commitDeletionEffects(finishedRoot, nestedMountedAncestor, child);
    child = child.sibling;
  }
}

/**
 * 递归遍历 Fiber 树,执行 Fiber 节点的副作用
 * @param {*} parentFiber fiber 节点
 * @param {*} root 根节点
 */
function recursivelyTraverseMutationEffects(parentFiber, root) {
  // 先把父节点上该删除的节点都删除
  const deletions = parentFiber.deletions;
  if (deletions !== null) {
    for (let i = 0; i < deletions.length; i++) {
      const childToDelete = deletions[i];
      commitDeletionEffects(root, parentFiber, childToDelete);
    }
  }
  // 再去处理剩下的子节点
  if (parentFiber.subtreeFlags & MutationMask) {
    let child = parentFiber.child;
    while (child) {
      commitMutationEffectsOnFiber(child, root);
      child = child.sibling;
    }
  }
}

在提交阶段中需要注意,这时的操作都是针对真实 DOM 的,所以删除时需要找到真实 DOM 父节点,以调用 DOM 方法删除。且不能直接删除该节点,需递归删除子节点,以便后续支持声明周期。