v-for 中的 key
问题
Vue 的 v-for 为什么需要 key?key 有什么作用?为什么不推荐用 index 作为 key?
答案
key 是 Vue 用于识别节点身份的特殊属性,在 v-for 渲染列表时,key 帮助 Vue 的 Diff 算法准确判断哪些节点可以复用、哪些需要移动或删除。
key 的作用
没有 key 的问题
<template>
<!-- ❌ 没有 key -->
<li v-for="item in list">
{{ item.name }}
<input v-model="item.input" />
</li>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const list = ref([
{ id: 1, name: 'A', input: '' },
{ id: 2, name: 'B', input: '' },
{ id: 3, name: 'C', input: '' }
]);
// 在第一个输入框输入 "Hello",然后删除第一项
// 预期:输入框内容跟随项目一起删除
// 实际:第一项被删除,但 "Hello" 还在(错误的 DOM 复用)
function removeFirst() {
list.value.shift();
}
</script>
使用唯一 key
<template>
<!-- ✅ 使用唯一 key -->
<li v-for="item in list" :key="item.id">
{{ item.name }}
<input v-model="item.input" />
</li>
</template>
Diff 算法中 key 的作用
// 简化的 Diff 逻辑
function patchKeyedChildren(
oldChildren: VNode[],
newChildren: VNode[]
) {
// 1. 建立 key -> index 的映射
const keyToOldIdx = new Map<any, number>();
for (let i = 0; i < oldChildren.length; i++) {
const key = oldChildren[i].key;
if (key != null) {
keyToOldIdx.set(key, i);
}
}
// 2. 遍历新节点
for (const newChild of newChildren) {
// 3. 通过 key 找到对应的旧节点
const oldIdx = keyToOldIdx.get(newChild.key);
if (oldIdx !== undefined) {
// 找到了:复用节点,更新内容
patch(oldChildren[oldIdx], newChild);
} else {
// 没找到:创建新节点
mount(newChild);
}
}
}
为什么不用 index 作为 key?
使用 index 作为 key 会导致以下问题:
<template>
<!-- ❌ 使用 index 作为 key -->
<li v-for="(item, index) in list" :key="index">
{{ item.name }}
</li>
</template>
问题 1:删除/插入时的错误复用
// 原始列表
// [A, B, C] -> key: [0, 1, 2]
// 删除 A 后
// [B, C] -> key: [0, 1]
// Diff 算法的判断:
// key=0: A -> B (内容变了,更新 DOM)
// key=1: B -> C (内容变了,更新 DOM)
// key=2: C -> 无 (删除)
// 实际发生了 2 次更新 + 1 次删除,而不是 1 次删除
问题 2:表单/组件状态错乱
<template>
<div v-for="(item, index) in list" :key="index">
<span>{{ item.name }}</span>
<input v-model="item.value" />
<button @click="remove(index)">删除</button>
</div>
</template>
<script setup lang="ts">
const list = ref([
{ id: 1, name: 'A', value: '' },
{ id: 2, name: 'B', value: '' },
{ id: 3, name: 'C', value: '' }
]);
function remove(index: number) {
list.value.splice(index, 1);
}
</script>
在第一个输入框输入 "Hello" 后删除第一项:
| 操作 | 使用 id 作为 key | 使用 index 作为 key |
|---|---|---|
| 删除前 | A[Hello], B[], C[] | A[Hello], B[], C[] |
| 删除后 | B[], C[] ✅ | B[Hello], C[] ❌ |
问题 3:Transition 动画异常
<template>
<TransitionGroup name="list">
<!-- index 导致动画不正确 -->
<li v-for="(item, index) in list" :key="index">
{{ item.name }}
</li>
</TransitionGroup>
</template>
什么时候可以用 index?
在以下特殊情况下,使用 index 是安全的:
<template>
<!-- ✅ 列表是静态的,不会增删改 -->
<li v-for="(tab, index) in staticTabs" :key="index">
{{ tab.name }}
</li>
<!-- ✅ 列表项是纯展示,无状态、无组件 -->
<span v-for="(tag, index) in tags" :key="index">
{{ tag }}
</span>
<!-- ✅ 列表项没有唯一 ID,且不会重排 -->
<option v-for="(option, index) in options" :key="index">
{{ option }}
</option>
</template>
最佳实践
<template>
<!-- 1. 使用数据的唯一标识 -->
<li v-for="user in users" :key="user.id">
{{ user.name }}
</li>
<!-- 2. 组合字段生成唯一 key -->
<li v-for="item in items" :key="`${item.type}-${item.id}`">
{{ item.name }}
</li>
<!-- 3. 如果没有 id,可以在数据获取时生成 -->
<script setup lang="ts">
import { nanoid } from 'nanoid';
const list = ref(
rawData.map(item => ({
...item,
_key: nanoid()
}))
);
</script>
<li v-for="item in list" :key="item._key">
{{ item.name }}
</li>
</template>
key 的其他用途
强制重新渲染组件
<template>
<!-- 改变 key 强制组件重新创建 -->
<UserProfile :key="userId" :id="userId" />
</template>
<script setup lang="ts">
const userId = ref(1);
// 切换用户时,组件会完全重新创建
function switchUser(newId: number) {
userId.value = newId;
}
</script>
重置表单状态
<template>
<!-- key 变化时,Input 组件重置 -->
<Input :key="formKey" v-model="value" />
<button @click="reset">重置</button>
</template>
<script setup lang="ts">
const formKey = ref(0);
const value = ref('');
function reset() {
formKey.value++; // 改变 key,组件重新创建
}
</script>
常见面试问题
Q1: v-for 为什么需要 key?
答案:
key 帮助 Vue 的 Diff 算法识别节点身份,实现:
- 准确复用:通过 key 精确匹配新旧节点
- 最小化 DOM 操作:只更新变化的部分
- 保持组件状态:确保状态与正确的节点关联
- 正确的 Transition 动画:动画应用到正确的元素
// 没有 key:按顺序就地复用(可能复用错误)
// 旧: [A, B, C] -> 新: [B, C, A]
// Vue 认为:A->B(更新), B->C(更新), C->A(更新) = 3 次更新
// 有 key:根据 key 匹配
// Vue 认为:A 移动到末尾 = 1 次移动
Q2: 为什么不能用随机数作为 key?
答案:
每次渲染都生成新的随机数,导致:
<!-- ❌ 每次渲染 key 都不同 -->
<li v-for="item in list" :key="Math.random()">
{{ item.name }}
</li>
问题:
- 无法复用:每次 key 都是新的,所有节点都被认为是新增
- 性能下降:全部销毁重建,失去 Diff 优化
- 状态丢失:组件状态每次都重置
Q3: key 可以重复吗?
答案:
不可以。重复的 key 会导致渲染错误和警告:
<!-- ❌ 重复的 key -->
<template>
<li v-for="item in list" :key="item.type">
{{ item.name }}
</li>
</template>
<script setup>
// 多个 item 可能有相同的 type
const list = [
{ type: 'fruit', name: 'Apple' },
{ type: 'fruit', name: 'Banana' }, // key 重复!
];
</script>
Vue 会发出警告:Duplicate keys detected。
Q4: key 的值有什么要求?
答案:
| 要求 | 说明 |
|---|---|
| 唯一性 | 同一父元素下,key 必须唯一 |
| 稳定性 | 同一数据项的 key 不应该变化 |
| 简单类型 | 应该是 string 或 number |
| 避免动态生成 | 不要用 Math.random()、Date.now() 等 |
// ✅ 好的 key
:key="item.id" // 数据库 ID
:key="item.uuid" // UUID
:key="`${type}-${id}`" // 组合键
// ❌ 不好的 key
:key="index" // 可能导致问题
:key="Math.random()" // 每次都变化
:key="Date.now()" // 每次都变化
:key="item" // 对象作为 key
Q5: 不同的 v-for 可以用相同的 key 吗?
答案:
可以。key 只需要在同一父元素的同一 v-for 中唯一:
<template>
<!-- 这两个 v-for 可以有相同的 key 值,因为它们在不同的父元素下 -->
<ul>
<li v-for="item in listA" :key="item.id">{{ item.name }}</li>
</ul>
<ul>
<li v-for="item in listB" :key="item.id">{{ item.name }}</li>
</ul>
</template>
但如果在同一父元素下:
<!-- ❌ 同一父元素下 key 可能冲突 -->
<ul>
<li v-for="item in listA" :key="item.id">A: {{ item.name }}</li>
<li v-for="item in listB" :key="item.id">B: {{ item.name }}</li>
</ul>
<!-- ✅ 添加前缀区分 -->
<ul>
<li v-for="item in listA" :key="'a-' + item.id">A: {{ item.name }}</li>
<li v-for="item in listB" :key="'b-' + item.id">B: {{ item.name }}</li>
</ul>