人人都会AI编程

同层比较原则:深度优先遍历,不跨层对比

更新时间:2026-07-11

虚拟 DOM 的 Diff 算法有一个核心约束:只对比同一层级的节点,不跨层级对比。这是 Vue 和 React 等主流框架共同遵循的策略,也是 Diff 算法能保持线性的关键所在。

为什么必须同层比较

想象一下,如果 Diff 算法允许跨层对比,那么一个 <div> 可能被移动到完全不同的 DOM 树位置,算法就需要尝试所有可能的节点匹配组合,时间复杂度会达到 O(n³),在真实应用中这将导致不可接受的性能灾难。而绝大多数前端界面更新并不涉及跨层级的节点移动——组件的结构在运行时基本是稳定的,你更多是在添加、删除或修改同一层级的列表项。

同层比较将复杂度降到了 O(n),让虚拟 DOM 的 Diff 在毫秒级内完成。它的规则很简单:当对比两个虚拟 DOM 树时,先比第一层,再深入对比子节点,一旦发现节点类型不同,直接删除重建,绝不尝试到兄弟层级里找“可能移过来的节点”。

Vue 中的实际运作方式

Vue 的 patch 过程严格遵循同层遍历:

<!-- 旧虚拟 DOM 树 -->
<div>                <!-- level 1 -->
  <p>Hello</p>       <!-- level 2 -->
  <span>A</span>     <!-- level 2 -->
</div>

<!-- 新虚拟 DOM 树 -->
<div>                <!-- level 1:对比 → 相同 div,继续深入 -->
  <span>A</span>     <!-- level 2:对比旧 p → 类型不同(p vs span),删除旧 p,创建新 span -->
  <span>B</span>     <!-- level 2:对比旧 span → 类型相同,复用 DOM,只更新文本 -->
</div>

在上面的例子中,比对完第一层(两个 div 相同),开始遍历第二层子节点:

  1. 新子节点 <span> 与旧子节点 <p> 标签不同,直接卸载旧 <p> 并创建新的 <span>
  2. 新子节点第二个 <span> 与旧子节点第二个 <span> 类型相同,则复用该 DOM 元素,仅更新其文本内容。

Vue 不会因为“旧树里已经有了一个 span”而尝试把那个 span 挪到新树的第一个位置,也不会去比较<p><span>是否可以通过某种“转换”来复用——不同类型,直接重建。这看似粗暴,但换来了极快的判断速度和极低的内存开销。

列表场景的特殊处理:key 属性

同层比较在遇到列表时,需要一个key来辅助识别节点是否可以复用。没有 key 时,Vue 会使用“就地复用”策略,按顺序逐个对比:

<!-- 旧 -->
<li>A</li>
<li>B</li>
<li>C</li>

<!-- 新(插入一条到最前面) -->
<li>D</li>
<li>A</li>
<li>B</li>
<li>C</li>

无 key 时,Vue 会依次对比:旧[0] vs 新[0] → A vs D → 文本不同,更新 A 为 D;旧[1] vs 新[1] → B vs A → 更新 B 为 A……最终所有节点都被更新了一次,而其实只需要新增一个 <li>D</li> 并保留原节点。

有了唯一的 key,Vue 就能在同一层级内进行更智能的节点复用:它先对比新旧两组的 key 值,找出可复用的节点并移动位置,只创建新 key 对应的 DOM,删除废弃的。这个过程发生的范围仍然是当前这个列表的层级内,不会跳到父级或子级中去。

对开发者的实际启示

  1. 保持组件结构稳定,不要做跨层级的 DOM 移动。如果确实需要在视觉上把一段内容从 A 区域移动到 B 区域,不要用跨层的新旧虚拟 DOM 结构来“希望 Diff 能识别”,而是通过数据驱动(改变数据渲染位置)或使用<Teleport>等专门机制来保证结构的同构。
  2. 列表渲染时务必绑定稳定且唯一的 key,并且不要用数组索引作为 key。这样做 Vue 才能在同一层内精准识别节点身份。
  3. 知道“类型不同即重建”的代价:如果你频繁切换一个元素的标签类型(如从<section>变为<article>),该组件内部的子组件会被全部销毁再重建,丢失私有状态。如果需要保留状态,应保持标签类型不变,改用v-if / v-show或动态组件来控制显示。

总结一句话

同层比较就是 Diff 的“车道线”:算法只在自己这一条道里比较和调整节点顺序,绝不会跑到上一道或下一道去串门。这既保证了运行效率,也让你在写模板时养成“结构对齐”的好习惯。