Vue 列表渲染中 key 的作用

Vue 列表渲染中 key 的作用

在 Vue 的模板中,几乎所有 v-for 列表渲染的示例都会写上 :key 属性,比如:

1
2
3
<li v-for="item in list" :key="item.id">
{{ item.name }}
</li>

Vue 官方文档也强调:在使用 v-for 时一定要提供 key
那么,key 到底有什么作用?为什么不建议使用 index 作为 key?

本文从虚拟 DOM diff 算法入手,结合具体例子说明:

  • Vue 为何需要 key
  • 使用不当会带来哪些具体问题?
  • 在实际项目中如何为列表选择合适的 key

一、虚拟 DOM 与列表 diff

Vue(无论 2 还是 3)在渲染时都会生成 虚拟 DOM(VNode)树。当状态更新时,Vue 会:

  1. 基于最新数据生成一棵新的虚拟 DOM 树
  2. 将新旧两棵树进行 diff 比较
  3. 找出差异后,最小化地更新真实 DOM

在列表场景中,diff 的核心就是:如何判断“旧节点”与“新节点”是否是同一个元素?

key 的作用就是帮助 Vue 更准确、高效地识别“同一个节点”。


二、没有 key 时,Vue 怎样 diff 列表?

假设有一个简单列表:

1
list = ["A", "B", "C"];

渲染为:

1
2
3
<li>A</li>
<li>B</li>
<li>C</li>

然后我们在开头插入一个元素 "X"

1
list = ["X", "A", "B", "C"];

如果没有 key,Vue 会采用一种近似的“就地复用”策略:

  • 认为“第一个 li 还在原位,只是内容从 A 变成 X”
  • 第二个 li 从 B 变成 A
  • 第三个 li 从 C 变成 B
  • 最后再新增一个 li 渲染 C

也就是说,Vue 会尽量复用原来的 DOM 节点,而不是认为每个旧节点都“右移一位”。

在某些场景下,这种复用是没有问题的,但在“带状态”的复杂节点中,就会引发 bug。


三、带状态的列表:没有 key 会发生什么?

例如一个带输入框的列表:

1
2
3
4
5
<ul>
<li v-for="item in list">
<input v-model="item.text" />
</li>
</ul>

此时每一行的 <input> 都有自己的“内部状态”(光标位置、输入法 composition 状态等)。
当我们在开头插入元素时,如果没有 key:

  • Vue 会“就地复用”原 DOM 节点
  • 原本第 1 个 <input> 的 DOM 节点会被当作“新第 1 个项”的 DOM
  • 结果可能出现:
    • 光标跳到意料之外的位置
    • 输入中的内容突然跑到下一行
    • 表单验证/动画状态错乱

用户体验会非常糟糕。


四、有 key 时,Vue 如何工作?

当我们为每个列表项提供稳定的 key(如 item.id):

1
2
3
<li v-for="item in list" :key="item.id">
<input v-model="item.text" />
</li>

Vue 在 diff 阶段会:

  1. 通过 key 构建“旧节点 key → 索引”的映射
  2. 对新列表中每个节点,根据 key 去映射表中查找“是否存在对应旧节点”
  3. 如果存在,认为是“同一个节点”,可以复用 DOM;否则认为是新增/删除

当在开头插入新元素时:

  • 原本的 A/B/C 每个都带有自己的 key,不会再错位复用
  • Vue 会意识到“原来的 A 现在在索引 1 处”,而不是“第 0 个 DOM 直接改成 X”

结果:

  • 每个 <input> 的 DOM 节点与其“逻辑项”一一对应
  • 用户输入状态不会混乱

五、为什么不推荐使用 index 作为 key?

看一个典型例子:

1
2
3
<li v-for="(item, index) in list" :key="index">
<input v-model="item.text" />
</li>

当我们在列表中间插入一项时:

  • 新项的 index 与后面的项互相“挤占”,导致所有后续项的 index 发生变化
  • Vue 会认为“第 n 项的 key 仍然是 n”,于是就地复用
  • 从 DOM 层面看来,“第 n 行的 input 节点”被复用了给另一条数据

这等价于“没有 key”时的就地复用策略。

只有当列表在整个生命周期中“只增不删/不插入,只 push 到末尾”时,使用 index 作为 key 才不会产生错乱,但这也极大地限制了列表的变更方式。

因此实践中推荐:

  • 总是使用业务上稳定的、唯一的 ID 作为 key
  • 避免使用 index/随机数/Math.random 作为 key

六、key 还能带来什么性能上的好处?

在使用高效的 diff 算法时,key 的存在可以帮助:

  • 快速判断节点是否可以复用
  • 对新旧列表做“最长递增子序列(LIS)”优化,最小化 DOM 操作次数

简单说:

  • 有稳定 key:Vue 可以更聪明地“移动 + 复用节点”
  • 无 key 或 key 不稳定:Vue 只能退化为“就地更新 + 尾部增删”,有时会做更多不必要的操作

虽然在小列表中性能差异不明显,但在大量节点、复杂组件树中,正确使用 key 是非常重要的性能基础。


七、Vue 中 key 的最佳实践

1. 使用业务 ID

1
2
3
<li v-for="item in list" :key="item.id">
{{ item.name }}
</li>

特点:

  • id 通常来自数据库或后端系统,在列表生命周期内具有稳定唯一性

2. 没有 ID 时,尽量构造稳定 key

比如:

1
2
3
<li v-for="todo in todos" :key="todo.title + '-' + todo.createdAt">
{{ todo.title }}
</li>

只要能够保证“同一条逻辑数据”的 key 不随渲染变更而变化即可。

3. 不要用随机数

1
2
3
4
<!-- 不要这样写 -->
<li v-for="item in list" :key="Math.random()">
{{ item.name }}
</li>

每次渲染时 key 都会变化,Vue 会认为这是“完全不同的一批节点”,直接全部重新创建/销毁,完全失去复用意义。


八、总结

在 Vue 列表渲染中,key 的作用可以概括为:

  • 帮助 Vue 在虚拟 DOM diff 过程中精准识别节点,从而:
    • 保证组件内部状态(输入框、动画等)不会“错位”
    • 提升 diff 效率,减少不必要的 DOM 操作
  • 避免因为位置变化导致的“就地复用”问题,尤其是在:
    • 列表中插入/删除元素
    • 列表项内部有带状态的子组件或原生控件时

实践建议:

  1. v-for 必须:key
  2. 优先使用业务稳定 ID 作为 key
  3. 避免使用 index / Math.random() 这类不稳定或无意义的 key。

理解了这些原理,再看“Vue 列表必须写 key”的规范,就不再只是“面向报错编程”,而是基于虚拟 DOM diff 机制做出的工程选择。


Vue 列表渲染中 key 的作用
https://sunjc.vip/2025/04/30/Vue列表渲染中key的作用/
作者
Sunjc
发布于
2025年4月30日
许可协议