React 的 Diff 算法是协调(Reconciliation)过程的核心,其核心目标是高效对比新旧虚拟 DOM(或 Fiber 节点)的差异,最终只更新必要的 DOM 部分。与传统树形结构的全量对比(时间复杂度 O (n³))不同,React Diff 基于启发式策略将复杂度优化至 O(n),其具体实现逻辑可拆解为「层级对比」「节点类型校验」「列表复用用策略」三个核心环节,以下是详细实现细节:

一、整体流程:分层对比与阶段划分

React Diff 的执行时机是在协调阶段(Reconciliation Phase),基于 Fiber 架构的「WorkInProgress 树」与「Current 树」进行对比。整体流程遵循「分层对比」原则:只对比同一层级的节点,不跨层级对比(这是降低复杂度的关键)。

具体步骤可分为:

  1. 从根 Fiber 节点开始,逐层向下对比(深度优先遍历)。
  2. 对每个层级的节点,先判断是否可复用(基于 type 和 key)。
  3. 若可复用,更新节点属性(props)并递归对比其子节点。
  4. 若不可复用,标记旧节点为「删除」,创建新节点并标记为「插入」。

整个过程通过 Fiber 节点的 child(子节点)、sibling(兄弟节点)、return(父节点)指针实现遍历,避免了栈递归的性能问题(Fiber 架构特性)。

二、核心对比逻辑:节点复用的判断标准

1. 节点类型与 key 的双重校验

判断两个节点(旧节点 oldFiber 和新节点 newChild)是否可复用,是 Diff 算法的第一步,核心依据是 type 和 key 的一致性

javascript

运行

// 简化逻辑:判断节点是否可复用
function isSameNode(oldFiber, newChild) {
  // 1. 对比 key(key 不存在时视为 undefined,需同时不存在才匹配)
  // 2. 对比 type(元素类型,如 'div'、FunctionComponent)
  return oldFiber.key === newChild.key && oldFiber.type === newChild.type;
}
  • 若 type 不同:直接判定为不可复用,旧节点及其子树会被标记为「删除(Deletion)」,新节点会被创建并标记为「插入(Placement)」。
    • 例:旧节点是 <div>,新节点是 <p> → 直接替换整个节点及子树。
  • 若 type 相同但 key 不同:视为不同节点,旧节点删除,新节点插入(即使内容相似)。
    • 例:列表中两个 <li key="1"> 和 <li key="2"> → 即使内容相同,也会被视为不同节点。
  • 若 type 和 key 都相同:判定为可复用,进入「属性更新」和「子节点对比」阶段。

2. 可复用节点的属性更新

当节点可复用(type 和 key 匹配)时,需要对比新旧节点的 props 差异,生成属性更新的副作用(Update):

javascript

运行

// 简化逻辑:计算 props 差异并标记更新
function updateNodeProps(oldFiber, newProps) {
  const oldProps = oldFiber.memoizedProps;
  const propDiff = {};
  // 1. 找出新 props 中与旧 props 不同的属性
  for (const key in newProps) {
    if (newProps[key] !== oldProps[key]) {
      propDiff[key] = newProps[key];
    }
  }
  // 2. 找出旧 props 中存在但新 props 中不存在的属性(需删除)
  for (const key in oldProps) {
    if (!(key in newProps)) {
      propDiff[key] = undefined; // 标记为删除
    }
  }
  // 3. 若有差异,标记副作用
  if (Object.keys(propDiff).length > 0) {
    oldFiber.effectTag |= Update; // 标记为需要更新
    oldFiber.pendingProps = newProps; // 存储新 props
  }
}

这一步只会更新有差异的属性(如 classNamestyle 变化),避免全量替换属性带来的性能浪费。

三、子节点对比:列表场景的核心优化

父节点可复用后,需要对比其子节点列表(oldChildren 和 newChildren)。子节点对比是 Diff 算法中最复杂的部分,尤其是列表场景(如 map 生成的子节点),需要处理增删、排序、移动等操作。

React 针对子节点列表的对比逻辑分为「单节点对比」和「多节点对比」,其中多节点对比(列表)是优化重点。

1. 单节点对比(子节点数量为 1)

若父节点只有一个子节点,直接使用 isSameNode 判断:

  • 可复用:更新属性,递归对比该子节点的子节点。
  • 不可复用:删除旧子节点,插入新子节点。

2. 多节点对比(列表场景)

当子节点是列表(长度 ≥ 2)时,React 通过「key 映射表」和「位置索引追踪」实现高效复用,核心步骤如下:

步骤 1:构建新子节点的 key→索引映射表

首先遍历新子节点列表(newChildren),创建 key 到索引的映射(keyToNewIndexMap),目的是快速判断旧节点是否存在于新列表中:

javascript

运行

// 构建新节点的 key 映射表
const keyToNewIndexMap = new Map();
for (let i = 0; i < newChildren.length; i++) {
  const child = newChildren[i];
  if (child.key !== null) {
    keyToNewIndexMap.set(child.key, i); // key → 新列表中的索引
  }
}
步骤 2:遍历旧子节点,标记复用或删除

遍历旧子节点列表(oldChildren),对每个旧节点 oldFiber

  • 若旧节点的 key 不在 keyToNewIndexMap 中 → 标记为「删除(Deletion)」。
  • 若 key 存在且 type 匹配 → 复用节点,记录其在新列表中的索引(newIndex)。
  • 若 key 存在但 type 不匹配 → 标记旧节点为「删除」,后续会插入新节点。

同时,为每个可复用的节点记录 newIndex,用于后续判断是否需要移动位置。

javascript

运行

const oldChildren = oldFiber.child;
let lastPlacedIndex = 0; // 追踪已处理节点的最大旧索引
let newIndex = 0;

while (oldChildren) {
  const oldKey = oldChildren.key;
  // 查找当前旧节点在新列表中的索引
  if (keyToNewIndexMap.has(oldKey)) {
    newIndex = keyToNewIndexMap.get(oldKey);
    // 检查 type 是否匹配
    if (oldChildren.type === newChildren[newIndex].type) {
      // 可复用:更新属性,递归对比子节点
      updateNodeProps(oldChildren, newChildren[newIndex].props);
      reconcileChildren(oldChildren, newChildren[newIndex].children);
      
      // 记录新索引,用于判断是否需要移动
      oldChildren.index = newIndex;
      
      // 判断是否需要移动(核心逻辑)
      if (newIndex < lastPlacedIndex) {
        // 新位置在已处理节点的左侧 → 需要移动
        oldChildren.effectTag |= Placement;
      } else {
        // 新位置在已处理节点的右侧 → 无需移动
        lastPlacedIndex = newIndex;
      }
    } else {
      // type 不匹配 → 删除旧节点
      oldChildren.effectTag |= Deletion;
    }
  } else {
    // key 不在新列表中 → 删除旧节点
    oldChildren.effectTag |= Deletion;
  }
  oldChildren = oldChildren.sibling; // 遍历下一个兄弟节点
}
步骤 3:处理新列表中新增的节点

遍历新子节点列表,对未在旧列表中匹配到的节点(即 keyToNewIndexMap 中存在但未被复用的节点),创建新的 Fiber 节点并标记为「插入(Placement)」:

javascript

运行

for (let i = 0; i < newChildren.length; i++) {
  const newChild = newChildren[i];
  // 检查该新节点是否已被复用(通过 key 映射表判断)
  if (!isAlreadyReused(newChild, keyToNewIndexMap)) {
    // 创建新 Fiber 节点
    const newFiber = createFiber(newChild);
    newFiber.return = oldFiber; // 关联父节点
    newFiber.effectTag = Placement; // 标记插入
    // 加入 WorkInProgress 树
    addToWorkInProgressTree(newFiber);
  }
}
列表对比的核心逻辑:如何判断节点是否需要移动?

通过 lastPlacedIndex(已处理节点在旧列表中的最大索引)判断节点位置是否需要调整:

  • 若当前节点在新列表中的索引 newIndex  lastPlacedIndex → 说明节点在新列表中的位置比之前处理的节点更靠后,无需移动,更新 lastPlacedIndex 为 newIndex
  • 若 newIndex < lastPlacedIndex → 说明节点在新列表中被前移了,需要标记为「移动(Placement)」,后续在 Commit 阶段调整 DOM 位置。

示例:旧列表:[A(key:1), B(key:2), C(key:3), D(key:4)](索引 0,1,2,3)新列表:[B(key:2), A(key:1), D(key:4), C(key:3)](索引 0,1,2,3)

  • 处理 B:newIndex=0lastPlacedIndex 初始为 0 → 0 ≥ 0 → 无需移动,lastPlacedIndex 改为 1(B 在旧列表的索引是 1)。
  • 处理 A:newIndex=1,1 < 1(lastPlacedIndex)→ 需要移动,标记 Placement。
  • 处理 D:newIndex=2,2 ≥ 1 → 无需移动,lastPlacedIndex 改为 3(D 在旧列表的索引是 3)。
  • 处理 C:newIndex=3,3 < 3 → 需要移动,标记 Placement。

最终操作:B 和 D 不动,A 和 C 移动位置。

四、特殊场景处理

1. 无 key 节点的对比

若节点没有 key,React 会将其视为「匿名节点」,此时仅通过 type 对比,且按位置顺序复用:

  • 若新旧节点 type 相同 → 复用,更新属性。
  • 若 type 不同 → 旧节点删除,新节点插入。

风险:无 key 或使用 index 作为 key 时,列表增删元素可能导致 key 对应关系错乱,引发节点复用错误(如输入框状态丢失)。

2. 跨层级节点移动

由于 React Diff 是「分层对比」,若节点从一个父节点移动到另一个父节点(跨层级),React 会直接删除旧节点并重建,而非移动(即使 key 相同)。

:旧结构:<div><span key="1">A</span></div>新结构:<p><span key="1">A</span></p>→ 由于父节点 div 和 p 类型不同,旧 span 会被删除,新 span 会被重建。

五、Diff 算法的输出:副作用链(Effect List)

Diff 过程中,所有需要执行的 DOM 操作(插入、更新、删除、移动)都会被标记到 Fiber 节点的 effectTag 属性中,最终通过 nextEffect 指针串联成「副作用链」。

在 Commit 阶段,React 会遍历副作用链,依次执行对应的 DOM 操作,确保所有变更一次性应用到真实 DOM(避免中间状态)。

六、总结

React Diff 算法的核心实现逻辑可概括为:

  1. 分层对比:只对比同一层级节点,不跨层级,降低复杂度。
  2. 节点复用判定:通过 type 和 key 双重校验,快速判断节点是否可复用。
  3. 列表优化:通过 key 映射表和 lastPlacedIndex 追踪位置,高效处理增删、移动操作,避免不必要的节点重建。
  4. 副作用标记:将差异操作标记为 effectTag,最终通过副作用链批量执行 DOM 操作。

这一算法在保证大多数场景高效的前提下,将时间复杂度优化至 O (n),是 React 性能优化的核心基础。理解其逻辑有助于开发者写出更符合 React 优化策略的代码(如合理设置 key、避免跨层级移动节点)。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐