一个非常经典和重要的 Vue 面试题: 为什么不能用 `index` 作为 `key`

51 阅读4分钟

场景还原与问题分析

描述的场景是这样的:

  1. 初始状态:有一个列表数据 ['A', 'B', 'C'],通过 v-for 循环渲染到页面上,每个列表项前面有一个复选框(Checkbox)。

    • 项0: A [ ]
    • 项1: B [ ]
    • 项2: C [ ]
  2. 用户操作:用户勾选了第一项 A 的复选框,然后点击“删除”按钮,意图删除 A

  3. 期望结果:删除 A 后,列表变为 ['B', 'C']。并且之前选中的状态应该被清除,或者跟随数据项。

    • 项0: B [ ]
    • 项1: C [ ]
  4. 实际结果(Bug)A 确实被删除了,列表变成了 ['B', 'C']。但是,高亮(选中)状态却留在了现在第一项 B

    • 项0: B [X] <-- 被错误地选中了
    • 项1: C [ ]

根本原因:为什么不能用 index 作为 key

这个 Bug 的产生,正是由于在 v-for 中使用了数组的 index(索引)作为 key

核心原理:Vue 的虚拟 DOM Diff 算法和“就地复用”策略

  1. key 的作用key 是 Vue 在虚拟 DOM 算法中用于识别一个节点的唯一标识。当列表发生变化时,Vue 会尽可能高效地更新真实的 DOM。它会比较新旧虚拟 DOM 树,并尝试复用已有的 DOM 元素。

  2. 使用 index 作为 key 时发生了什么?

    • 初始渲染

      • 数据: ['A', 'B', 'C']
      • 索引 (key): 0, 1, 2
      • Vue 建立起一个映射:key=0 -> 内容 A + 复选框状态(未选中);key=1 -> 内容 B + 复选框状态(未选中);key=2 -> 内容 C + 复选框状态(未选中)。
    • 用户操作:用户选中了 A(即 key=0 的项)。此时,key=0 对应的 DOM 节点状态变为“选中”。

    • 删除数据 A

      • 新数据变为:['B', 'C']
      • 新的索引 (key) 变为:0, 1
      • Vue 开始 Diff 对比:
        • 旧 VNode: [key0, key1, key2]
        • 新 VNode: [key0, key1]
        • Vue 发现 key0key1 仍然存在!它不会去关心 key 对应的数据内容是否已经改变,它只是简单地根据 key 来复用已有的 DOM 节点。
        • 因此,它复用了旧的 key=0 的 DOM 节点(这个节点之前显示 A,并且是选中状态),只是把其文本内容更新为新的数据 B
        • 同样,它复用了旧的 key=1 的 DOM 节点(之前显示 B,未选中),更新其文本内容为 C
        • key=2 的节点被丢弃。

结论:由于 index 自身的不稳定性(当数组开头或中间的元素被增删时,后续元素的 index 会全部改变),导致 Vue 复用了错误的 DOM 节点,从而使得视图状态(如复选框选中、输入框内容、滚动位置等)与预期数据项错位


正确的解决方案:使用唯一的 id 作为 key

正确的做法是,为数据列表中的每一个项提供一个唯一且稳定的标识符(通常是后端返回的 id)。

// 数据格式
items: [
  { id: 101, name: 'A' },
  { id: 102, name: 'B' },
  { id: 103, name: 'C' }
]
<!-- 在模板中 -->
<div v-for="item in items" :key="item.id">
  <!-- 复选框和内容 -->
  <input type="checkbox"> {{ item.name }}
</div>

现在让我们看看使用 id 作为 key 时会发生什么:

  • 初始渲染

    • 映射:key=101 -> 内容 A(未选中);key=102 -> 内容 B(未选中);key=103 -> 内容 C(未选中)。
  • 用户操作:用户选中了 Akey=101)。

  • 删除数据 A

    • 新数据:[{id:102, name:'B'}, {id:103, name:'C'}]
    • Vue 开始 Diff 对比:
      • 旧 VNode: [key101, key102, key103]
      • 新 VNode: [key102, key103]
      • Vue 发现 key101 在新列表中不存在了,会直接销毁对应的 DOM 节点(连同其选中状态一起销毁)。
      • key102key103 在新列表中依然存在,且位置没变,Vue 会复用它们。它们的内部状态(未选中)保持不变。

结果:删除 A 后,列表正确显示为 BC,且两者都是未选中状态。视图状态与数据完美同步


面试回答总结

你可以这样向面试官阐述:

“在 Vue 的 v-for 中,不推荐使用 index 作为 key,主要是因为 index 不是一个稳定不变的标识符。

当列表数据发生动态变化,比如在数组开头或中间进行增删操作时,数组的 index 会发生变化。而 Vue 的虚拟 DOM Diff 算法会基于 key 来识别节点并进行高效的 DOM 复用。

如果使用 index 作为 key,在数据变化后,Vue 会错误地复用那些 key 值没变但实际内容已经改变的 DOM 节点。这会导致视图状态(例如复选框的选中状态、输入框的内容等)与底层数据不一致,出现状态错乱的 Bug。

因此,最佳实践是始终为每一项数据提供一个唯一且稳定的 id 作为 key,这样可以确保数据、key 和 DOM 节点状态之间的正确绑定,避免上述的渲染问题。”