列表更新与 key 机制
列表、key 与过渡动画 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
V02 讲 key 时留了伏笔,这一节把它讲透:Vue 的列表 diff 到底怎么用 key 匹配节点、为什么 index 当 key 会串状态、什么时候可以用 index、以及带内部状态列表项的正确写法。
问题场景
一个可排序的表格:点表头按名称排序,每行有一个「备注」输入框。排序后,输入框里的内容跟着行一起移动了——看起来正确,但如果你用的是 :key="index",内容其实是被「重新分配」给了新位置的行,组件状态和业务数据错位。更隐蔽的 bug:动画、焦点、滚动位置都会跟着错位的节点走。
核心解释
Vue 更新子节点列表时有两种模式:
- 无 key(就地复用):按位置比较,第 1 个旧节点复用给第 1 个新节点,只更新文本内容。简单高效,但「节点的身份」是位置。
- 有 key(身份匹配):按 key 找到「还是原来那个」的节点,复用它的 DOM 和组件实例,只移动位置。节点的身份是 key 值。
<script setup>
import { ref } from "vue";
const rows = ref([
{ id: "u1", name: "阿吉", note: "" },
{ id: "u2", name: "小北", note: "" },
{ id: "u3", name: "老陈", note: "" },
]);
function sortByName() {
rows.value = [...rows.value].sort((a, b) => a.name.localeCompare(b.name));
}
</script>
<template>
<button @click="sortByName">按名字排序</button>
<table>
<tbody>
<tr v-for="row in rows" :key="row.id">
<td>{{ row.name }}</td>
<td><input v-model="row.note" placeholder="备注" /></td>
</tr>
</tbody>
</table>
</template>
这里 :key="row.id" 是唯一正确写法:排序时 Vue 把「阿吉」那一行整体移动到新位置,输入框的 DOM、焦点、光标位置都跟着走。如果改成 :key="index",位置 0 的 DOM 被复用给排序后的第 1 行,输入框里残留的是上一行的内容——因为 v-model 绑定的是新行的 note(空),但 DOM 值被复用时浏览器可能保留旧显示,直到下次渲染才纠正,视觉上就是「错位一帧」或「内容串行」。
什么时候可以用 index:列表完全静态(渲染后不增删排序)时,index 没问题;或者列表项是纯展示、没有内部状态和组件实例时,影响也可忽略。但只要满足「会变 + 有状态」两个条件之一,就必须用业务 id。
key 的另外两个用法:
- 强制重建组件:给组件一个会变的 key(
:key="userId"),key 变了 Vue 销毁旧实例、创建新实例,用于「切换用户时重置表单」这类需求。 <template v-for>上的 key:key 要写在<template>上而不是内部元素上,否则 Vue 警告。
<template>
<template v-for="row in rows" :key="row.id">
<tr>
<td>{{ row.name }}</td>
</tr>
<tr class="detail">
<td>备注:{{ row.note || "无" }}</td>
</tr>
</template>
</template>
示例拆解
做一个能观察 diff 行为的实验:同一份数据,两组列表,一组 index 作 key、一组 id 作 key,每行带一个「自增计数」按钮(组件内部状态),然后打乱顺序:
<script setup>
import { ref } from "vue";
const items = ref([
{ id: 1, label: "A" },
{ id: 2, label: "B" },
{ id: 3, label: "C" },
]);
function shuffle() {
items.value = [...items.value].sort(() => Math.random() - 0.5);
}
</script>
<template>
<button @click="shuffle">打乱顺序</button>
<h3>index 作 key</h3>
<ul>
<li v-for="(item, i) in items" :key="i">
{{ item.label }} <RowCounter />
</li>
</ul>
<h3>id 作 key</h3>
<ul>
<li v-for="item in items" :key="item.id">
{{ item.label }} <RowCounter />
</li>
</ul>
</template>
RowCounter 是一个内部持有 count 的小组件(点击 +1)。操作:给 A、B、C 分别点出不同的计数,然后点「打乱顺序」。index 版本里计数留在原地(A 的 3 次跑到新位置的行上),id 版本里计数跟着字母走。这个对比就是 key 语义的直观证明。
动手任务
- 完成上面的实验,截图或记录两个列表打乱后的计数分布,用一句话解释差异。
- 把
:key="index"换成:key="item.id"修复一个你项目里(或本站练习里)的列表,确认输入框内容不再串行。 - 用
:key="userId"实现「切换用户时重置表单」:两个用户按钮,切换后表单清空。
常见错误
- index 当 key 且有增删/排序:DOM 与组件状态错位,输入框、动画、焦点全乱。
- key 用随机数:每次渲染 key 都变,所有节点被销毁重建,性能暴跌且丢状态。
- key 重复:控制台警告「Duplicate keys」,渲染结果不可预测。
- key 写在
<template v-for>的子元素上:应写在<template>上,否则 Vue 3 报编译警告。
检查与复盘
- 能画出「无 key 就地复用」和「有 key 身份匹配」的对比
- 能判断一个列表该用什么 key,并说出反例后果
- 知道「用 key 强制重建组件」这个技巧
参考资料
- Vue 官方文档「列表渲染」:维护状态与 key 的官方说明
- Vue 官方文档「深入虚拟 DOM」:diff 算法概览
读完这一课了?
标记完成后,工作台会安排接下来的学习。