快速、简洁讲明 Vue 中 v-for 循环 key 的作用
原文通过列表插入、数组 index 和 Diff 图示说明
key。本文保留这些例子,并修正“有 key 就只渲染新增项”“没有 key 就一定很慢”等绝对化说法。
一、key 到底解决什么问题
key 的核心作用不是一个单纯的性能开关,而是告诉 Vue:
新旧列表中的哪些 VNode 代表同一个业务实体。
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
当列表发生插入、删除或排序时,Vue 可以通过稳定的 key 找到旧节点对应的新位置,从而复用并移动合适的 DOM 或组件实例。
如果没有 key,Vue 默认采用就地 patch策略:按当前位置复用节点,并把该位置更新成新数据。这种方式对简单、无状态的文本列表很高效;但如果子节点包含输入框、组件内部状态、焦点或过渡状态,就可能复用错节点对应的临时状态。
key 并不保证:
- 只有新增项会创建 DOM;
- 组件完全不会重新渲染;
- 每次更新只产生一次 DOM 操作;
- 所有场景都比无 key 更快。
它首先表达的是节点身份,性能收益是正确复用和移动带来的结果。
二、为什么通常不推荐使用 index 作为 key
下面是一个列表:
const list = [
{ id: 1, num: 1 },
{ id: 2, num: 2 },
{ id: 3, num: 3 },
]
<div v-for="(item, index) in list" :key="index">
{{ item.num }}
</div>
1. 在末尾追加
如果只在末尾追加,并且列表不会排序、插入或删除中间项目:
const nextList = [
{ id: 1, num: 1 },
{ id: 2, num: 2 },
{ id: 3, num: 3 },
{ id: 4, num: '新增加的数据 4' },
]
原有 index 0、1、2 仍然对应原有实体,index 3 是新节点。在这种受限场景中使用 index 通常不会产生明显问题。
2. 在中间插入
如果在第二项前插入新数据:
const nextList = [
{ id: 1, num: 1 },
{ id: 4, num: '新增加的数据 4' },
{ id: 2, num: 2 },
{ id: 3, num: 3 },
]
使用 index 时,旧 key 和新 key 的对应关系是:
旧 key 0 → 新 key 0:仍是 id 1
旧 key 1 → 新 key 1:旧 id 2 被复用为新 id 4
旧 key 2 → 新 key 2:旧 id 3 被复用为新 id 2
旧 key 3 → 新 key 3:新建 id 3
Vue 并不会因为 index 变化就简单地“重新渲染三条完整 DOM”。更准确地说,它会按 key 复用当前位置的节点,再更新节点内容;如果节点是组件或带有输入状态,旧实体的本地状态可能被错误地保留在新实体上。
例如:
<input
v-for="item in list"
:key="index"
v-model="item.name"
>
当列表前面插入项目时,输入框 DOM 可能按位置复用,焦点、未提交输入或子组件状态就不一定跟随业务实体移动。
3. 使用稳定的业务 ID
<li v-for="item in list" :key="item.id">
{{ item.num }}
</li>
新旧列表的身份匹配是:
旧 id 1 → 新 id 1:复用并保持位置
旧 id 2 → 新 id 2:复用并移动到新位置
旧 id 3 → 新 id 3:复用并移动到新位置
新 id 4:创建新节点
这更符合业务实体的身份,也更适合组件列表、可编辑表单、拖拽排序和过渡动画。
三、key 和 Virtual DOM Diff
当数据变化时,Vue 会重新生成相关组件的 VNode,并由 renderer 比较新旧 VNode。简化理解:
- 主要进行同层比较;
- 节点类型不同,通常卸载旧节点并创建新节点;
- 节点类型相同且 key 相同,才有机会复用;
- keyed children 可以通过 key 建立旧节点到新位置的映射;
- Vue 2 使用双端比较等策略,Vue 3 对 keyed children 还会使用最长递增子序列优化移动。

没有 key 的就地 patch
没有 key 时,Vue 通常按位置复用:
旧:A B C
新:A F B C
位置 0:A 复用
位置 1:旧 B 更新成 F
位置 2:旧 C 更新成 B
位置 3:创建 C
对于只有文本的列表,这可能是合理且高效的;对于有状态的子树,则要使用稳定 key 表达身份。
使用 key 的身份匹配
旧:A B C
新:A F B C
key A:复用
key B:移动
key C:移动
key F:创建


Vue 可能通过移动已有 DOM 来完成结果,也可能因为属性、组件类型、过渡或编译标记而执行额外更新。图示用于说明身份匹配,不代表内部每一步的精确源码流程。
四、key 的使用规则
1. 使用稳定、唯一且原始类型的值
<div v-for="item in items" :key="item.id" />
key 应在同一列表范围内唯一、稳定,通常使用字符串或数字。不要使用:
<!-- ❌ 每次渲染都变化,导致节点不断重建 -->
<div v-for="item in items" :key="Math.random()" />
<!-- ❌ 对象 key 不是推荐的稳定原始值 -->
<div v-for="item in items" :key="item" />
重复 key 会导致匹配不确定和运行时警告。
2. template v-for 的 key
Vue 3 中使用 <template v-for> 渲染多个兄弟节点时,可以把 key 放在 template 上:
<template v-for="item in items" :key="item.id">
<li>{{ item.name }}</li>
<li class="divider" role="presentation" />
</template>
3. key 可以主动触发重建
改变组件 key 会让 Vue 把它视为新的组件实例:
<Editor :key="documentId" :document="document" />
这可以用于切换文档时重置编辑器内部状态,但不能把它当作普通的刷新按钮。频繁改变 key 会丢失组件状态并增加创建/销毁成本。
4. 不是所有列表都必须强行添加 key
Vue 官方建议在可能的情况下为 v-for 提供 key,尤其是:
- 列表包含组件;
- 列表包含表单输入或其他 DOM 临时状态;
- 列表会排序、插入、删除;
- 列表参与 Transition/TransitionGroup。
如果只是简单的无状态文本列表,并且明确希望使用就地 patch,不加 key 也可以是合理选择。规范要求和团队代码风格仍应保持一致。
五、Vue 与 React 的类比
React 的 map 列表同样需要 key 来表达元素身份,Vue 的 v-for 也是类似思想。但两者的具体 reconciliation 实现、编译优化和 API 不完全相同,不能因为都有 key 就认为 Diff 过程完全一致。
六、总结
key的第一职责是标识列表节点身份;- 没有 key 时,Vue 默认按位置就地 patch;
- 稳定 key 允许 Vue 在列表插入、删除、排序时复用和移动正确节点;
- index 只适合不会重排、插入、删除且没有内部状态的受限列表;
- key 不等于“只有一项会渲染”,也不保证所有场景都更快;
- 使用业务实体 ID,避免随机值和重复 key;
- 修改 key 会主动销毁旧实例并创建新实例。
参考资料
原文作者:橙某人。原文关于 index、业务 ID、Virtual DOM 和 Diff 的主线予以保留;错误的 YAML 代码块、格式化残片、无关图片、重复推广链接以及“key 只让新增项渲染”等错误表述已清理或修正。