列表渲染:v-for 与 key
模板语法与常用指令 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
掌握 v-for 的三种遍历形式(数组、对象、数字范围),理解 :key 为什么不是可选项,学会用 v-for 配合条件与 template 渲染复杂列表结构。学完你能正确渲染任何列表,并避免「输入框内容串行」这类经典 bug。
问题场景
一个待办列表:每行有复选框、可编辑标题、删除按钮。列表能渲染,但勾选第一项时第三项也变了;删除中间一项后,下面所有行的输入框内容整体上移一位。这不是模板写错了,是 :key 用错了——Vue 靠 key 判断「哪个节点还是原来那个」,没有 key 或用了 index 当 key,节点复用就会错位。
核心解释
v-for 的基础语法是 item in items,也支持 (item, index) 和解构:
<script setup>
const fruits = ["苹果", "香蕉", "橙子"];
const user = { name: "阿吉", role: "前端" };
</script>
<template>
<ul>
<li v-for="(fruit, index) in fruits" :key="fruit">
{{ index + 1 }}. {{ fruit }}
</li>
</ul>
<ul>
<li v-for="(value, key) in user" :key="key">{{ key }}:{{ value }}</li>
</ul>
<p v-for="n in 3" :key="n">第 {{ n }} 段</p>
</template>
:key 的作用是给每个节点一个稳定身份。Vue 的更新策略是「同层比较、尽量复用」:没有 key 时按位置复用,有 key 时按身份匹配。对静态文本列表,用 index 当 key 问题不大;但只要列表项包含有内部状态的元素(input、组件实例、动画),就必须用业务唯一 id:
<script setup>
import { ref } from "vue";
const todos = ref([
{ id: 1, title: "读 V02", done: true },
{ id: 2, title: "写练习", done: false },
{ id: 3, title: "复习 key", done: false },
]);
function remove(id) {
todos.value = todos.value.filter((t) => t.id !== id);
}
</script>
<template>
<ul>
<li v-for="todo in todos" :key="todo.id">
<label>
<input type="checkbox" v-model="todo.done" />
{{ todo.title }}
</label>
<button @click="remove(todo.id)">删除</button>
</li>
</ul>
</template>
这里 :key="todo.id" 是唯一正确选择:删除 id=2 时,Vue 按 id 匹配,1 还是 1、3 还是 3,各自的 DOM 状态(勾选、焦点)跟着走。如果换成 :key="index",删除后原来的第三项变成第二项,DOM 被复用给新数据,勾选状态就会「串行」。
v-for 和条件混用要小心。Vue 3 中同一元素上 v-if 优先级更高,拿不到循环变量;正确写法有两种:用 computed 先过滤数据,或在外层用 <template v-for> 包裹:
<script setup>
import { computed, ref } from "vue";
const todos = ref([
{ id: 1, title: "已完成项", done: true },
{ id: 2, title: "待办项", done: false },
]);
const pending = computed(() => todos.value.filter((t) => !t.done));
</script>
<template>
<ul>
<template v-for="todo in todos" :key="todo.id">
<li v-if="!todo.done">{{ todo.title }}</li>
</template>
</ul>
<p>待办 {{ pending.length }} 项</p>
</template>
示例拆解
把「勾选串行」的 bug 完整复现一遍,这是本节最重要的实验:
<script setup>
import { ref } from "vue";
const items = ref([
{ id: "a", label: "第一项" },
{ id: "b", label: "第二项" },
{ id: "c", label: "第三项" },
]);
const checked = ref([]);
function removeFirst() {
items.value = items.value.slice(1);
}
</script>
<template>
<button @click="removeFirst">删除第一项</button>
<ul>
<!-- 故意用 index 当 key:删除后勾选状态会错位 -->
<li v-for="(item, index) in items" :key="index">
<label>
<input type="checkbox" v-model="checked" :value="item.id" />
{{ item.label }}
</label>
</li>
</ul>
<p>已选:{{ checked.join("、") || "无" }}</p>
</template>
操作步骤:勾选「第二项」,点「删除第一项」,观察「已选」文本和复选框的对应关系。再把 :key="index" 改成 :key="item.id",重复同样操作,行为就正确了。这个实验做一遍,比背十遍规则有用。
动手任务
- 完成上面的实验,记录改 key 前后的差异,用一句话解释原因。
- 给列表加一个「全选 / 取消全选」按钮,用
checked数组驱动(提示:全选时赋值为所有 id)。 - 用
v-for渲染一个 3×3 的表格(嵌套两层v-for),key 分别用行号和单元格内容。
常见错误
- 用 index 当 key 又有增删:删除/排序后 DOM 复用错位,输入框、勾选、动画状态串行。
- key 重复:同一个 key 出现两次,Vue 渲染出重复节点并警告「Duplicate keys」。key 必须全局唯一(至少在兄弟节点间唯一)。
v-if和v-for写在同一元素:循环变量在v-if里不可用,Vue 3 直接报错。- 直接修改数组元素却不触发更新:
todos.value[0] = newTodo在 Vue 3 的ref数组里其实是能追踪的,但如果你把数组整体替换后又持有旧引用,就会渲染旧数据。规范做法是整体替换或使用数组方法(push/splice)。
检查与复盘
- 能写出数组、对象、数字范围三种
v-for形式 - 能解释 key 的匹配机制,并说出 index 当 key 会出问题的具体场景
- 能写出
template v-for配合v-if的正确结构
参考资料
- Vue 官方文档「列表渲染」:key 的官方解释与
v-for全部形式 - Vue 官方文档「条件渲染」:与
v-for的优先级说明
读完这一课了?
标记完成后,工作台会安排接下来的学习。