前言:
v-for 是 Vue3 中最高频的列表渲染指令,要写 Vue 页面,几乎天天都要和 v-for 打交道:表格、下拉选项、卡片列表、动态表单...... 全靠它循环渲染。很多人觉得 v-for 很简单,但是开发中经常遇到:列表数据错乱、视图不更新、控制台报错、输入框内容串值等玄学问题。 这些问题大多不是 Vue bug,而是 v-for 使用细节没注意。
一、v-for 常用用法:
1. 遍历数组(最常用)
语法:v-for="(item, index) in array"
- item:数组当前项
- index:当前下标索引
xml
<template>
<ul>
<li v-for="(item, index) in userList" :key="item.id">
{{ index + 1 }}. {{ item.name }} - {{ item.age }}
</li>
</ul>
</template>
<script setup>
import { ref } from 'vue'
const userList = ref([
{ id: 1, name: '张三', age: 22 },
{ id: 2, name: '李四', age: 25 },
{ id: 3, name: '王五', age: 21 }
])
</script>
2. 遍历对象
语法:v-for="(value, key, index) in object"
顺序记住:值、键名、索引
xml
<template>
<div v-for="(val, key, idx) in info" :key="key">
{{ idx }} | {{ key }}:{{ val }}
</div>
</template>
<script setup>
import { reactive } from 'vue'
const info = reactive({
name: '小明',
gender: '男',
address: '上海'
})
</script>
3. 遍历数字,循环固定次数
可以直接传入数字,从 1 开始循环:
xml
<!-- 输出:1 2 3 4 5 -->
<span v-for="n in 5" :key="n">{{ n }}</span>
适合做分页页码、固定数量占位骨架屏。
4. template 循环,不生成多余 DOM
<template> 是占位标签,渲染后不会生成真实 DOM,想要循环多个标签又不想外层包一层 div,就用它:
xml
<template>
<template v-for="item in goodsList" :key="item.id">
<p class="title">{{ item.title }}</p>
<p class="price">¥{{ item.price }}</p>
</template>
</template>
5. 循环自定义组件
循环组件和普通标签写法一致,key 同样不能少,props 正常传递:
ini
<GoodsCard
v-for="item in goodsList"
:key="item.id"
:goods="item"
/>
6. 列表过滤:优先使用 computed
不要直接在 v-for 里写过滤逻辑,模板里尽量简洁,复杂过滤交给计算属性,缓存性能更好:
xml
<template>
<li v-for="item in hotList" :key="item.id">{{ item.title }}</li>
</template>
<script setup>
import { ref, computed } from 'vue'
const goodsList = ref([
{ id: 1, title: '商品A', hot: true },
{ id: 2, title: '商品B', hot: false },
{ id: 3, title: '商品C', hot: true }
])
// 只展示热门商品
const hotList = computed(() => {
return goodsList.value.filter(item => item.hot)
})
</script>
二、开发高频坑点 + 解决方案:
坑点 1:v-for 和 v-if 写在同一个标签上(头号大坑),Vue3 官方文档明确不建议 v-for 和 v-if 共存于同一个元素。
❌ 错误写法:
xml
<!-- 不推荐!v-for 优先级高于 v-if,会先循环全部再判断,性能差,逻辑容易出问题 -->
<li v-for="item in list" v-if="item.isShow" :key="item.id">
{{ item.name }}
</li>
两种推荐方案:
1. template 包裹,v-for 放外层 template
ini
<template v-for="item in list" :key="item.id">
<li v-if="item.isShow">{{ item.name }}</li>
</template>
2. 优先 computed 提前过滤数组(性能最优)
坑点 2:key 绑定 index,列表增删、拖拽后数据 DOM 错位
❌ 错误写法:
css
<li v-for="(item, index) in list" :key="index">{{ item.name }}</li>
问题点: 当数组中间删除、插入、拖拽排序时,index 会重新变化,Vue Diff 算法会复用错误 DOM,输入框内容串掉、数据和界面对不上,这个坑在带表单的列表里非常致命。
推荐写法:
使用业务唯一 ID
css
<li v-for="item in list" :key="item.id">{{ item.name }}</li>
只有一种特殊情况可以用 index:列表是静态写死的,永远不会新增、删除、排序。业务动态列表一律不要用 index 做 key。
坑点 3:直接通过数组下标赋值,视图不更新
❌ 容易失效写法
css
// 直接下标赋值,Vue3 Proxy 虽然比Vue2好,但复杂场景依然会出现不更新
list.value[0] = { id: 99, name: '测试' }
解决办法:
- 使用 splice 修改
php
list.value.splice(0, 1, { id: 99, name: '测试' })
- 替换数组引用
ini
list.value = [...list.value]
坑点 4:key 值重复,控制台警告,渲染错乱
同一个循环内 key 重复,Vue 抛出警告,DOM 随机错乱,很难复现排查。
xml
<!-- 两条数据id相同,key重复 -->
<li v-for="item in list" :key="item.id">{{ item.name }}</li>
原因:后端返回数据 id 重复、模拟数据写重了。
解决:保证 key 在当前循环内唯一;没有唯一 id 时前端生成 uuid。
坑点 5:循环组件收集 ref,直接 ref 拿不到组件实例
❌ 错误写法
ini
<MyInput v-for="item in list" :key="item.id" ref="inputRef" />
这样写 inputRef 只会拿到最后一个组件实例。
推荐写法:
Vue3 使用函数式 ref 收集
xml
<MyInput v-for="item in list" :key="item.id" :ref="setRef" />
<script setup>
const compList = []
const setRef = (el) => {
if (el) compList.push(el)
}
</script>
坑点 6:循环输入框 v-model 绑定同一个变量,全部联动
❌ 错误:
ini
<input v-for="item in list" :key="item.id" v-model="inputVal" />
所有输入框绑定同一个变量,改一个全部一起变。
推荐写法:
绑定循环项自身属性
ini
<input v-for="item in list" :key="item.id" v-model="item.name" />
坑点 7:异步接口数据,初始值为 null /undefined,页面直接报错
❌ 错误:
接口还没返回,list 默认是 null,v-for 循环会报迭代器错误:Cannot read property 'Symbol(Symbol.iterator)' of null
css
<li v-for="item in list" :key="item.id">{{ item.name }}</li>
推荐方案:
初始值给空数组
csharp
const list = ref([])
// 接口返回后再赋值
或者增加空状态判断
xml
<ul v-if="list.length">
<li v-for="item in list" :key="item.id">{{ item.name }}</li>
</ul>
<div v-else>暂无数据</div>
坑点 8:遍历响应式对象,动态新增属性视图不更新
ini
const user = reactive({ name: '张三' })
// 直接新增属性,部分场景不触发更新
user.age = 20
解决办法:
替换对象引用
css
Object.assign(user, { age: 20 })
// 或者
const newUser = { ...user, age: 20 }
Object.assign(user, newUser)
三、最佳实践总结:
v-for必须携带key,优先业务唯一 ID,尽量避免 index- 不要把
v-for和v-if放在同一个标签;过滤列表优先 computed - 尽量避免直接下标修改数组,优先 splice 或者替换引用
- 异步列表初始值设置为空数组,防止渲染报错
- 需要收集循环组件 ref,使用函数 ref
- template 标签用来分组渲染,减少多余 DOM
- 模板逻辑保持简洁,复杂计算、过滤交给 script
v-for 看着简单,但是很多坑都是上线操作增删排序的时候才暴露,尤其是动态表单、可拖拽列表场景。只要把 key、v-if 和 v-for 的优先级、响应式更新这三点把控好,基本能避开绝大多数问题。