Vue3 列表渲染
日常开发中”把一组数据铺成一组相同结构的页面元素”是最常见的需求:文章列表、商品卡片、下拉选项……这全靠 v-for 完成。
这篇讲 v-for 的四种写法、key 到底有什么用,以及修改数组时最不容易出错的姿势。
先记住 v-for 的基本句式,后面所有写法都是它的变体。
基本语法
v-for="别名 in 数据源",会重复渲染所在元素,每次循环从”数据源”取出一个元素,放进”别名”变量:
| 部分 | 说明 |
|---|---|
| 别名 | 当前这一项的变量名,随便起(item、u、row),见名知意 |
in |
固定写法,写 of 效果也一样 |
| 数据源 | 要遍历的内容:数组、对象、数字、字符串都行 |
<li v-for="item in list">{{ item }} 元</li>
item 只在所在元素及其子元素里有效,代表”这一次循环取到的那一项”。
遍历数组
<script setup>
import { ref } from 'vue'
const list = ref([
{ id: 1, name: '苹果', price: 5 },
{ id: 2, name: '香蕉', price: 3 }
])
</script>
<template>
<ul>
<li v-for="item in list" :key="item.id">
{{ item.name }}:{{ item.price }} 元
</li>
</ul>
</template>
(key 先放着别管,它为什么重要、怎么选,下面 key 章节专门讲。)
带索引时,第二参数是下标:
<li v-for="(item, index) in list" :key="item.id">
{{ index }} - {{ item.name }}
</li>
修改数组
Vue3 的 ref([...]) 是深度响应式的,绝大多数数组操作都会自动触发表更新:
const list = ref([{ id: 1, name: '苹果' }, { id: 2, name: '香蕉' }])
// 都会触发更新:
list.value.push({ id: 3, name: '橙子' })
list.value.splice(1, 1)
list.value[0].name = '红苹果' // 改元素属性,OK
list.value[1] = { id: 2, name: '雪梨' } // 按下标替换,Vue3 也能侦测
filter、map、concat 这类方法返回新数组,不会原地改原数组。想要”过滤”的效果,要么 filter 的结果赋回去,要么用 computed 派生一份(官方推荐):
const keyword = ref('')
const filtered = computed(() =>
list.value.filter(item => item.name.includes(keyword.value))
)
其他可遍历对象
| 遍历目标 | 写法 | 参数 |
|---|---|---|
| 数组 | v-for="item in list" |
(item, index) |
| 对象 | v-for="value in obj" |
(value, key, index) |
| 数字范围 | v-for="n in 10" |
从 1 到 10 |
| 字符串 | v-for="ch in 'abc'" |
逐字符 |
<!-- 遍历对象:展示 user 的所有字段 -->
<div v-for="(value, key) in user" :key="key">
{{ key }}:{{ value }}
</div>
<!-- 生成 1..10 的页码 -->
<button v-for="n in 10" :key="n">{{ n }}</button>
为什么 v-for="n in 10" 生成的是 1 到 10,而不是 0 到 9?
Vue 的官方约定:范围从 1 开始。正好适合页码、星级这类从 1 计数的东西。
key
设计理念
响应式核心篇讲过:Vue 更新页面不是清空重画,而是尽量复用已有的 DOM,只改该改的地方。
要复用,Vue 就得先知道”对象是谁”——“新列表里的这一项,对应旧列表里哪一个 DOM?” key 就是回答这个问题的:它是一行数据的身份标识,Vue 靠它对号入座,决定复用哪块 DOM、移动哪些元素,而不至于认错人。
初次渲染时,按什么顺序铺开都能稳稳对号入座,真正的考验在列表变化之后:一旦增删、排序,纯靠”第几行”的关系就乱了,身份标识这时才显出必要。
错误示范
看一个真实会出错的例子,不写 key,或把 index 当 key。
先交代数据:这个列表来自一个数组,每项自带 id 和 name。为了演示”状态串位”,每行再放一个不绑定任何数据的输入框:
<script setup>
import { ref } from 'vue'
const list = ref([
{ id: 1, name: '苹果' },
{ id: 2, name: '香蕉' }
])
</script>
<template>
<ul>
<li v-for="item in list">{{ item.name }} <input /></li>
</ul>
</template>
模板里 v-for 把数组两项各渲染成一行,每行是”名称 + 一个输入框”。
用户在”苹果”那行往输入框里打了字,页面效果:
第 1 行:苹果 [输入框: 好吃] ← 用户打的内容
第 2 行:香蕉 [输入框: ]
现在往最前面插入「橙子」,数组变成:
插入前:苹果(id=1) → 香蕉(id=2)
插入后:橙子(id=3) → 苹果(id=1) → 香蕉(id=2)
把新旧顺序并排看,每一行的位置都变了(下表只标了位置,先不管 DOM/输入框):
| 位置 | 插入前 | 插入后 |
|---|---|---|
| 0 | 苹果 | 橙子(新来的,占了第 0 位) |
| 1 | 香蕉 | 苹果(从第 0 位被挤到了第 1 位) |
| 2 | 香蕉(从第 1 位被挤到了第 2 位) |
数据还是那份数据,每项的 id 也没变,只是行号全部对不上了。
如果不写 key,或用 index 当 key,Vue 只能按位置猜——“第一行还是那个第一行”:
| 渲染前 | 渲染后(不写 / index 作 key) |
|---|---|
| 0: 苹果 [好吃] | 0: 橙子 [好吃] ← 错位了! |
| 1: 香蕉 [ ] | 1: 苹果 [ ] |
| 2: 香蕉 [ ] |
第一行的输入框原是”苹果”的,现在数据变成了”橙子”,但输入框里还留着”好吃”——输入内容和行对不上了,这就是”复用认错人”的后果。
为什么文字变成了”橙子”,输入框还留着”好吃”?
- 文字是
{{ item.name }}由数据驱动的插值:数组第 0 项现在数据是”橙子”,Vue 就重算文本,把苹果换成橙子。 - 输入框里的”好吃”是用户自己敲进去的,没有绑定任何数据(这里不是
v-model)。Vue 只管它”管得着”的部分(数据所驱动的属性和文本),管不着的内容原样保留。
于是这行 DOM 被复用了,数据变了导致文本刷新,输入框这种非数据驱动的内容却残留着上一行的用户输入——错位就是这么来的。
正确实践
避免认错人,就给每项一个独立且不变的身份标识,通常直接用数据自带的 id:
<li v-for="item in list" :key="item.id">{{ item.name }}</li>
用唯一 id 当 key,Vue 按 id 认出”你还是你”,每项的变化清清楚楚:
| key(身份) | 插入前 | 插入后 |
|---|---|---|
| key=3 橙子 | —(还没来) | 全新一行 [ ] |
| key=1 苹果 | 第 0 行 [输入框: 好吃] | 第 1 行 [输入框: 好吃] ← 输入内容原样保留 |
| key=2 香蕉 | 第 1 行 [输入框: ] | 第 2 行 [输入框: ] |
同样是插入橙子,这次 Vue 靠 id 认出”苹果还是那行、香蕉还是那行”,于是这两行的输入框内容(含用户打的”好吃”)原样保留;只有新增的 key=3 橙子才是全新一行。
总结
key帮 Vue 把”数据项”和”DOM 行”一一对应,增删时不至于排错队。列表会增删/排序,或项内有输入框、勾选等状态时,必须用唯一 id 当 key。
纯展示、永不变化的静态列表用 index 也能跑,但养成写 id 的习惯更稳。
v-for 与 v-if 混用要注意
同一个元素上不要同时写 v-if 和 v-for。
在 Vue3 里 v-if 优先级比 v-for 高,也就是说 v-if 判断时根本拿不到 v-for 里的变量,写法上也会让 Vue 产生警告:
<!-- 错误示范:v-if 里访问不到 item -->
<li v-for="item in list" v-if="item.visible">{{ item.name }}</li>
<!-- 正确做法:用 computed 先过滤,再交给 v-for 渲染 -->
<li v-for="item in visibleList">{{ item.name }}</li>
<!-- 或者包一层 template,让 v-for 在外层 -->
<template v-for="item in list">
<li v-if="item.visible">{{ item.name }}</li>
</template>
想用 v-for 包着一组元素,就套 <template v-for="...">,它不会渲染成真实标签,上面第三种写法就是这个套路。
速查卡片
| 场景 | 写法 |
|---|---|
| 遍历数组 | v-for="(item, index) in list" |
| 遍历对象 | v-for="(value, key) in obj" |
| 范围 | v-for="n in 10" |
| 身份标识 | :key="item.id"(唯一 id) |
| 过滤/派生列表 | computed 返回新数组 |
| 多元素复用 | <template v-for="..."> 包裹 |
| 增删安全 | 优先 push / splice / 下标替换,别用 index 做 key |