在 Vue3 的 v-for 列表渲染中,key 的使用方式直接影响列表更新时的 DOM 行为,尤其是包含删除操作时,不同 key 策略会呈现不同的表现(甚至异常)。下面从「无 key」「key 为 index」「key 为唯一值」三种场景逐一分析,并结合删除操作的示例说明差异。
核心原理铺垫
Vue 的虚拟 DOM 对比(diff 算法)依赖 key 来识别节点的唯一性:
- 有唯一 key:Vue 能精准判断节点的增 / 删 / 移,只更新变化的 DOM;
- 无 key/key 为 index:Vue 无法识别节点唯一性,会通过「就地复用」策略更新 DOM,可能导致 DOM 与数据不匹配。
场景复现准备
先定义基础组件,包含一个列表和删除按钮,后续仅修改 v-for 的 key:
vue
<template>
<div>
<div v-for="(item, index) in list" :key="xxx"> <!-- 重点:xxx 替换为不同值 -->
<input type="text" v-model="item.name">
<button @click="deleteItem(index)">删除</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
// 初始化列表(每个项有唯一 id,模拟业务场景)
const list = ref([
{ id: 1, name: '张三' },
{ id: 2, name: '李四' },
{ id: 3, name: '王五' }
])
// 删除方法
const deleteItem = (index) => {
list.value.splice(index, 1)
}
</script>
场景 1:无 key(不写 :key)
表现
删除某一项后,输入框的内容会「错位」,DOM 看似更新但数据与视图不匹配。
示例过程
- 初始状态:输入框分别输入「张三」「李四」「王五」;
- 删除索引 1(李四);
- 结果:列表只剩两项,但输入框显示「张三」「王五」→ 看似正常?✘ 实际异常点:若列表项包含「状态绑定 / 组件实例」(比如输入框焦点、自定义组件内部状态),会出现错位。(补充:无 key 时 Vue 会按「节点位置」复用 DOM,删除索引 1 后,原索引 2 的 DOM 会被移到索引 1 位置,仅更新文本内容,但组件 / 输入框的内部状态会保留。)
本质
Vue 认为「节点位置」即唯一标识,直接复用 DOM 节点,仅更新节点的文本 / 属性,忽略数据的唯一性,若列表项有「非响应式状态」(如输入框焦点、组件内部变量),会导致状态错位。
场景 2:key 为 index(:key="index")
表现
删除操作后,输入框内容错位更明显(比无 key 更易复现),是日常开发中最易踩的坑。
示例过程
-
初始状态:输入框分别输入「张三」「李四」「王五」;
-
删除索引 1(李四);
-
结果:
- 数据层面:list 变为
[{id:1,name:'张三'}, {id:3,name:'王五'}]; - 视图层面:输入框显示「张三」「李四」(而非「王五」),DOM 与数据完全错位。
- 数据层面:list 变为
原因分析(关键)
| 操作前 | 操作后(删除索引 1) |
|---|---|
| 索引 0 → key0 → 张三 | 索引 0 → key0 → 张三(复用原 DOM,无变化) |
| 索引 1 → key1 → 李四 | 索引 1 → key1 → 王五(复用原索引 1 的 DOM,仅更新文本,但输入框的 v-model 绑定的是 item.name,为何错位?) |
| 索引 2 → key2 → 王五 | 索引 2 被删除 |
核心错位逻辑:当 key 为 index 时,删除索引 1 后,原索引 2 的项(id:3,name: 王五)会「占据」索引 1 的位置。Vue 的 diff 算法认为:
- key0(索引 0)的节点不变,复用;
- key1(索引 1)的节点需要更新,于是将原索引 1 的 DOM 节点的
item替换为新的索引 1 项(王五),但输入框的 DOM 节点是复用的,v-model 的绑定是「事后更新」,导致视觉上输入框内容未同步(或出现延迟 / 错位)。
极端案例(含组件状态)
若列表项是自定义组件(有内部状态):
vue
<!-- 自定义组件 -->
<template>
<div>{{ item.name }} - 内部状态:{{ innerState }}</div>
</template>
<script setup>
const props = defineProps(['item'])
const innerState = ref(Math.random()) // 组件内部状态
</script>
<!-- 列表使用 -->
<div v-for="(item, index) in list" :key="index">
<MyComponent :item="item" />
<button @click="deleteItem(index)">删除</button>
</div>
删除索引 1 后,原索引 2 的组件会复用原索引 1 的组件 DOM,内部状态(innerState)不会重置,导致「王五」显示的是「李四」组件的内部状态,完全错位。
场景 3:key 为唯一值(:key="item.id")
表现
删除操作后,DOM 精准更新,无任何错位,输入框 / 组件状态与数据完全匹配。
示例过程
-
初始状态:输入框输入「张三」「李四」「王五」;
-
删除索引 1(李四,id:2);
-
结果:
- 数据层面:list 变为
[{id:1,name:'张三'}, {id:3,name:'王五'}]; - 视图层面:直接移除 id:2 对应的 DOM 节点,剩余节点的 DOM 完全保留(输入框内容、组件状态均无错位)。
- 数据层面:list 变为
原因分析
Vue 通过唯一 key(item.id)识别节点:
- 删除 id:2 的项时,Vue 直接找到 key=2 的 DOM 节点并移除;
- 剩余项的 key(1、3)与原节点一致,复用 DOM 且状态不变;
- 无任何 DOM 复用错位,数据与视图完全同步。
本质
唯一 key 让 Vue 能精准匹配「数据项」和「DOM 节点」,diff 算法会:
- 对比新旧列表的 key 集合;
- 移除不存在的 key(如 2);
- 保留存在的 key(1、3),仅更新内容(若有变化);
- 新增的 key(若有)则创建新 DOM 节点。
三种场景对比表
| 场景 | 删除操作后的表现 | 底层逻辑 | 适用场景 |
|---|---|---|---|
| 无 key | 文本看似正常,组件 / 输入框状态可能错位 | 按位置复用 DOM,无唯一性识别 | 仅纯文本列表,无状态 / 输入框 |
| key 为 index | 输入框 / 组件状态明显错位,数据与视图不匹配 | 按索引复用 DOM,索引变化导致错位 | 临时静态列表(无增删改) |
| key 为唯一值 | 无错位,DOM 精准更新 | 按唯一标识匹配节点,精准增删 | 所有有增删改的列表(推荐) |
关键总结
- 禁止在有增删改的列表中使用 index 作为 key:这是 Vue 官方明确不推荐的做法,会导致 DOM 复用错位;
- 无 key 等同于 key 为 index:Vue 内部会默认使用 index 作为隐式 key,表现一致;
- 唯一 key 必须是数据本身的属性 :不能是临时生成的唯一值(如
Math.random()),否则每次渲染都会认为是新节点,导致 DOM 全量重建,性能极差; - 唯一 key 的选择:优先使用业务唯一标识(如 id、手机号、订单号),避免使用 index / 随机值。
扩展:Vue3 对 key 的优化
Vue3 的 diff 算法(PatchFlags)相比 Vue2 更高效,但key 的核心作用不变------ 唯一 key 仍是保证列表更新准确性的关键,Vue3 仅优化了「有 key 时的对比效率」,并未改变「无 key/index key 导致的错位问题」。
最终正确示例
vue
<template>
<div>
<div v-for="(item, index) in list" :key="item.id">
<input type="text" v-model="item.name">
<button @click="deleteItem(index)">删除</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const list = ref([
{ id: 1, name: '张三' },
{ id: 2, name: '李四' },
{ id: 3, name: '王五' }
])
const deleteItem = (index) => {
list.value.splice(index, 1)
}
</script>