六维教程

Vue3 列表渲染

日常开发中”把一组数据铺成一组相同结构的页面元素”是最常见的需求:文章列表、商品卡片、下拉选项……这全靠 v-for 完成。

这篇讲 v-for 的四种写法、key 到底有什么用,以及修改数组时最不容易出错的姿势。

先记住 v-for 的基本句式,后面所有写法都是它的变体。

基本语法

v-for="别名 in 数据源",会重复渲染所在元素,每次循环从”数据源”取出一个元素,放进”别名”变量:

部分 说明
别名 当前这一项的变量名,随便起(itemurow),见名知意
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 也能侦测

filtermapconcat 这类方法返回新数组,不会原地改原数组。想要”过滤”的效果,要么 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" 110
字符串 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,或把 indexkey

先交代数据:这个列表来自一个数组,每项自带 idname。为了演示”状态串位”,每行再放一个不绑定任何数据的输入框:

<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-forv-if 混用要注意

同一个元素上不要同时写 v-ifv-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
上一篇
Vue3 条件渲染
下一篇
Vue3 事件处理