一、前言
上篇 v-if 管"显示谁",这篇 v-for 管"显示一堆":任务清单、商品列表、表格行,全靠它。本篇重头戏是那个经典面试题------key 是干嘛的?为什么官方反复强调别用 index 当 key? 很多写了一年 Vue 的人也答不上来,今天用实验讲明白。
二、v-for 基础:遍历数组
语法 v-for="(item, index) in 数组",:key 别忘了:
vue
<template>
<ul>
<li v-for="(item, index) in fruits" :key="item.id">
{{ index }}. {{ item.name }} ------ ¥{{ item.price }}
</li>
</ul>
</template>
<script setup>
import { ref } from 'vue'
const fruits = ref([
{ id: 1, name: '苹果', price: 5 },
{ id: 2, name: '香蕉', price: 3 },
{ id: 3, name: '西瓜', price: 12 }
])
</script>
只要模板结构来自数据循环,就必须写 :key(不写会有警告)。
三、遍历对象 / 数字 / 字符串
v-for 不只能遍历数组,四个场景的参数顺序注意记:
vue
<template>
<!-- 1. 对象:(值, 键, 索引) ------ 值在前! -->
<li v-for="(value, key, index) in user" :key="key">
{{ key }}: {{ value }}(第 {{ index }} 个属性)
</li>
<!-- 2. 数字:从 1 到 5,常用于分页页码 -->
<span v-for="n in 5" :key="n">{{ n }} </span>
<!-- 3. 字符串:逐字符遍历 -->
<i v-for="(ch, i) in 'ABCD'" :key="i">{{ ch }}</i>
<!-- 4. 嵌套:乘法口诀式结构 -->
<div v-for="row in 3" :key="row">
<span v-for="col in 3" :key="col">{{ row }}-{{ col }} </span>
</div>
</template>
<script setup>
import { reactive } from 'vue'
const user = reactive({ name: '小明', age: 18, city: '北京' })
</script>
四、key 到底是干嘛的:虚拟 DOM 的"身份证"
你写的 template 会先变成虚拟 DOM(JS 对象描述的节点树),数据变化时生成新的虚拟 DOM,然后新旧对比(diff 算法),只把差异部分更新到真实 DOM。
问题来了:对比时,新列表的第 2 个节点,对应旧列表的第 2 个节点吗?key 就是节点间的对账凭证:
- 有 key:新旧列表按 key 匹配,谁对应谁一目了然,能复用的就地复用,只动真正变化的
- 没 key / 用 index:Vue 按位置"对号入座",位置对上了就认为没变------一旦列表顺序变化,就会大量错位更新
五、实验:为什么不能用 index 当 key
经典翻车现场------一个带输入框的任务列表,在中间插入一项:
vue
<template>
<ul>
<!-- 用 index 当 key 的版本 -->
<li v-for="(task, index) in tasks" :key="index">
{{ task }}
<input placeholder="这里输入点内容" />
<button @click="tasks.splice(index, 1)">删除</button>
</li>
</ul>
<button @click="tasks.unshift('🔥 新任务')">在头部插入新任务</button>
</template>
<script setup>
import { ref } from 'vue'
const tasks = ref(['学 Vue', '写博客', '面试准备'])
</script>
复现步骤:
- 在第 1、2 个输入框里分别输入
aaa、bbb - 点"在头部插入新任务"
- 见证翻车:输入框内容错位了------aaa 跑到了第二个框,bbb 跑到第三个框
原因:index 当 key 时,插入后每个位置的 index 都变了,但 key(0、1、2)还是 0、1、2。Vue 按 key 对比,认为"每个位置还是原来的节点,只是文本变了",于是就地更新文本,而输入框是非受控的(内部状态不跟着模板走),文本挪了、输入框没挪 → 错位。
把 :key="index" 换成 :key="task"(用唯一稳定值)再试:内容精确跟着自己的主人走,绝不错位。
结论记忆:key 必须是"这条数据的唯一稳定标识"(id),index 会随位置变化,等于没有身份证。
【截图位置:index 当 key 输入内容错位前后的对比图】
六、综合实战:任务清单(增删改过滤全家桶)
vue
<template>
<input v-model.trim="newTask" placeholder="输入任务" @keyup.enter="add" />
<button @click="add">添加</button>
<p>未完成 {{ undoneCount }} 项</p>
<ul>
<li v-for="task in tasks" :key="task.id">
<input type="checkbox" v-model="task.done" />
<span :class="{ done: task.done }">{{ task.text }}</span>
<button @click="remove(task.id)">删</button>
</li>
</ul>
<p v-if="tasks.length === 0">🎉 全部完成,无任务</p>
</template>
<script setup>
import { ref, computed } from 'vue'
let id = 0
const newTask = ref('')
const tasks = ref([
{ id: ++id, text: '学完 v-for', done: true },
{ id: ++id, text: '做任务清单', done: false }
])
const undoneCount = computed(() => tasks.value.filter(t => !t.done).length)
function add() {
if (!newTask.value) return
tasks.value.unshift({ id: ++id, text: newTask.value, done: false })
newTask.value = ''
}
function remove(id) {
tasks.value = tasks.value.filter(t => t.id !== id)
}
</script>
<style scoped>
.done { text-decoration: line-through; color: #999; }
</style>
数据驱动一切:id 唯一做 key,增删只改数组,视图自动同步。
【截图位置:任务清单添加、勾选删除线、删除的运行效果】
七、踩坑记录
- v-for 和 v-if 同元素 :Vue3 中 v-if 先执行、拿不到 item 直接报错(Vue2 是 v-for 先执行,行为都变了)。过滤需求一律交给 computed:
computed(() => list.value.filter(...)) - key 用 index:上面实验翻车了。有唯一 id 用 id;实在没有稳定 id 的静态展示列表才可勉强用 index
- key 用随机数 / Date.now():每次渲染 key 都不同,等于告诉 Vue"全是新节点",销毁重建性能灾难
- v-for 里改被遍历的数组:可以,Vue3 响应式数组在遍历中增删是安全的,但避免在回调里无脑 splice 造成无限循环
- 对象的遍历顺序:整数键会按数字升序排前面,其余按插入顺序------依赖遍历顺序做业务时要小心
八、今日小结
- v-for 四连:数组
(item, index)、对象(value, key)、数字、字符串 - key 是虚拟 DOM diff 的对账凭证,必须是唯一稳定值
- index 当 key 在"插入/删除 + 内部状态"场景必然错位
- v-for 过滤数据用 computed,别和 v-if 挤在一起
下篇预告
列表有了,用户点按钮、敲键盘怎么响应?下一篇 017 事件处理进阶------$event 是什么、事件修饰符 .stop/.prevent/.once 怎么让代码干净一半。