在Vue开发中,列表渲染是最常见的需求之一——从展示商品列表、用户列表,到渲染导航菜单、评论列表,几乎所有页面都离不开列表的呈现。Vue提供了v-for指令,专门用于基于源数据(数组、对象等)动态渲染列表,搭配响应式数据更新,能轻松实现列表的增删改查、过滤排序等交互效果。今天就从基础用法、核心原理到进阶避坑,全方位拆解Vue列表渲染,帮你写出高效、可维护的列表代码,彻底吃透v-for的使用精髓。
核心指令:v-for 基础用法——从数组到列表的快速渲染
v-for的核心作用是遍历源数据(数组、对象、范围值等),并根据遍历结果重复渲染指定的DOM模板。它的基础语法简洁易懂,最常用的场景就是遍历数组,实现列表的批量渲染。
1. 遍历数组:最基础的列表渲染
遍历数组时,v-for采用“item in items”的语法形式,其中items是源数据数组,item是当前迭代项的别名,相当于循环中的临时变量,仅在v-for绑定的元素及其内部可访问,类似函数的局部作用域。
实战示例:渲染用户列表(基础版)
<script setup>
import { ref } from 'vue';
// 模拟用户列表数据(响应式数组)
const users = ref([
{ id: 1, name: '张三', age: 22, gender: '男' },
{ id: 2, name: '李四', age: 24, gender: '女' },
{ id: 3, name: '王五', age: 21, gender: '男' },
{ id: 4, name: '赵六', age: 23, gender: '女' }
]);
</script>
<template>
<div class="user-list">
<h3>用户列表</h3>
<ul>
<!-- v-for遍历数组,item为当前用户对象 -->
<li v-for="user in users" :key="user.id" class="user-item">
姓名:{{ user.name }} | 年龄:{{ user.age }} | 性别:{{ user.gender }}
</li>
</ul>
</div>
</template>
<style>
.user-list {
margin: 20px 0;
padding: 15px;
border: 1px solid #eee;
border-radius: 4px;
}
.user-item {
margin: 8px 0;
padding: 8px;
background: #f8f9fa;
border-radius: 2px;
list-style: none;
}
</style>
2. 迭代索引:获取当前项的位置信息
除了迭代项本身,v-for还支持可选的第二个参数,用于获取当前项在数组中的索引(从0开始),语法为“(item, index) in items”。索引常用于列表的序号显示、删除指定位置元素等场景。
实战示例:带序号的用户列表
<script setup>
import { ref } from 'vue';
const users = ref([
{ id: 1, name: '张三', age: 22, gender: '男' },
{ id: 2, name: '李四', age: 24, gender: '女' },
{ id: 3, name: '王五', age: 21, gender: '男' },
{ id: 4, name: '赵六', age: 23, gender: '女' }
]);
</script>
<template>
<div class="user-list">
<h3>带序号的用户列表</h3>
<ul>
<!-- 第二个参数index为当前项的索引 -->
<li v-for="(user, index) in users" :key="user.id" class="user-item">
序号:{{ index + 1 }} | 姓名:{{ user.name }} | 年龄:{{ user.age }}
</li>
</ul>
</div>
</template>
<style>
/* 样式沿用上面的基础样式 */
</style>
3. 解构赋值:简化迭代项的访问
如果迭代项是对象,我们可以在v-for中使用解构赋值,直接提取对象的属性,避免频繁书写“item.xxx”,让模板更简洁。解构语法与JavaScript对象解构一致,支持按需提取需要的属性。
实战示例:解构赋值简化用户列表渲染
<script setup>
import { ref } from 'vue';
const users = ref([
{ id: 1, name: '张三', age: 22, gender: '男' },
{ id: 2, name: '李四', age: 24, gender: '女' },
{ id: 3, name: '王五', age: 21, gender: '男' },
{ id: 4, name: '赵六', age: 23, gender: '女' }
]);
</script>
<template>
<div class="user-list">
<h3>解构赋值简化列表渲染</h3>
<ul>
<!-- 解构user对象,直接提取name和age属性 -->
<li v-for="({ name, age }, index) in users" :key="users[index].id" class="user-item">
序号:{{ index + 1 }} | 姓名:{{ name }} | 年龄:{{ age }}
</li>
</ul>
</div>
</template>
4. 分隔符替代:of 与 in 的用法区别
v-for默认使用“in”作为分隔符,除此之外,还可以使用“of”作为分隔符,语法为“item of items”,这更接近JavaScript的迭代器语法(如for...of循环),功能与“in”完全一致,可根据个人习惯选择。
<template>
<div class="user-list">
<h3>使用of分隔符的列表</h3>
<ul>
<!-- 使用of替代in,功能完全一致 -->
<li v-for="user of users" :key="user.id" class="user-item">
姓名:{{ user.name }} | 性别:{{ user.gender }}
</li>
</ul>
</div>
</template>
扩展用法:v-for 遍历对象与范围值
v-for不仅可以遍历数组,还能遍历对象的属性和整数范围值,覆盖更多场景,比如渲染对象详情、生成固定数量的元素等。
1. 遍历对象:渲染对象属性列表
遍历对象时,v-for支持三个参数,分别是:当前属性值(value)、属性名(key)、索引(index),语法为“(value, key, index) in object”。遍历顺序基于Object.values()的返回值,确保一致性。
实战示例:渲染用户详情(对象遍历)
<script setup>
import { ref } from 'vue';
// 模拟单个用户对象(响应式)
const userInfo = ref({
name: '张三',
age: 22,
gender: '男',
job: '前端开发',
address: '北京市海淀区'
});
</script>
<template>
<div class="user-detail">
<h3>用户详情</h3>
<ul>
<!-- 遍历对象,获取属性值、属性名和索引 -->
<li v-for="(value, key, index) in userInfo" :key="key" class="detail-item">
{{ index + 1 }}. {{ key }}:{{ value }}
</li>
</ul>
</div>
</template>
<style>
.user-detail {
margin: 20px 0;
padding: 15px;
border: 1px solid #eee;
border-radius: 4px;
}
.detail-item {
margin: 6px 0;
list-style: none;
padding: 4px 0;
border-bottom: 1px dashed #eee;
}
</style>
2. 遍历范围值:生成固定数量的元素
v-for可以直接接受一个整数值,此时会基于1~n的范围重复渲染模板,常用于生成固定数量的元素,比如分页页码、星级评分等场景。注意:范围值的起始值是1,而非0。
实战示例:生成星级评分组件
<script setup>
import { ref } from 'vue';
// 模拟评分(1~5星)
const score = ref(4);
</script>
<template>
<div class="star-rating">
<h3>星级评分:{{ score }}星</h3>
<div class="stars">
<!-- 遍历1~5的范围,生成5个星星 -->
<span v-for="n in 5" :key="n" class="star" :class="{ active: n <= score }">
★
</span>
</div>
</template>
<style>
.star-rating {
margin: 20px 0;
padding: 15px;
border: 1px solid #eee;
border-radius: 4px;
}
.stars {
font-size: 24px;
color: #ccc;
}
.star.active {
color: #ffc107;
cursor: pointer;
}
</style>
3. template上的v-for:批量渲染多个元素
与template上的v-if类似,我们也可以在template标签上使用v-for,实现多个元素的批量渲染,而无需额外添加包裹容器(如div、ul)。template仅作为不可见的包装器,最终渲染结果不会包含该标签。
实战示例:批量渲染商品列表项(多元素)
<script setup>
import { ref } from 'vue';
// 模拟商品列表数据
const goods = ref([
{ id: 1, name: 'Vue实战教程', price: 99, stock: 100 },
{ id: 2, name: 'JavaScript进阶指南', price: 89, stock: 50 },
{ id: 3, name: '前端面试题库', price: 69, stock: 30 }
]);
</script>
<template>
<div class="goods-list">
<h3>商品列表</h3>
<!-- 用template包裹多个元素,批量渲染 -->
<template v-for="good in goods" :key="good.id">
<div class="goods-item">
商品名称:{{ good.name }} | 价格:¥{{ good.price }}
</div>
<div class="goods-stock">库存:{{ good.stock }}件</div>
<hr />
</template>
</div>
</template>
<style>
.goods-list {
margin: 20px 0;
padding: 15px;
border: 1px solid #eee;
border-radius: 4px;
}
.goods-item {
margin: 8px 0;
font-size: 16px;
}
.goods-stock {
margin: 4px 0;
color: #666;
font-size: 14px;
}
</style>
核心重点:key 属性的作用与使用规范
在使用v-for时,我们总会看到一个特殊的属性——key,它是Vue列表渲染中至关重要的一环,直接影响列表的渲染性能和正确性。很多初学者容易忽略key的使用,或者使用不当导致渲染异常,这一部分我们重点拆解key的核心作用。
1. key 的核心作用:跟踪DOM节点的标识
Vue默认采用“就地更新”策略渲染列表:当数组数据发生变化时,Vue不会重新创建所有DOM元素,而是就地更新每个元素的内容,以匹配数据的变化。这种策略高效,但在某些场景下(如列表项包含表单、子组件状态)会导致渲染异常。
key的作用就是给每个列表项分配一个唯一的标识,让Vue能够精确跟踪每个DOM节点对应的数据源,从而在数据变化时,准确地重用、移动或销毁DOM元素,避免渲染异常,提升渲染性能。
2. key 的使用规范(避坑重点)
- key 必须是唯一的:每个列表项的key值不能重复,否则会导致Vue无法正确跟踪节点,引发渲染错误;
- key 建议使用基础类型:优先使用字符串、数字等基础类型作为key,避免使用对象作为key(Vue无法准确判断对象的唯一性);
- key 不要使用索引:索引会随着数组的增删改查发生变化,当数组元素顺序改变时,key也会随之变化,导致Vue重新创建DOM,失去key的意义;
- template v-for>的key:当在template标签上使用v-for时,key必须绑定在template上,而非内部的DOM元素上。
实战示例:正确使用key的todo列表
<script setup>
import { ref } from 'vue';
// 模拟todo列表,用uuid作为唯一id(实际开发中可使用专门的id生成工具)
const todos = ref([
{ id: 'todo-1', text: '学习Vue列表渲染', done: false },
{ id: 'todo-2', text: '掌握key的使用', done: true },
{ id: 'todo-3', text: '完成实战案例', done: false }
]);
</script>
<template>
<div class="todo-list">
<h3>Todo列表(正确使用key)</h3>
<ul>
<li v-for="todo in todos" :key="todo.id" class="todo-item">
<input type="checkbox" v-model="todo.done" />
<span :class="{ done: todo.done }">{{ todo.text }}</span>
</li>
</ul>
</div>
</template>
<style>
.todo-list {
margin: 20px 0;
padding: 15px;
border: 1px solid #eee;
border-radius: 4px;
}
.todo-item {
margin: 8px 0;
list-style: none;
padding: 6px;
background: #f8f9fa;
border-radius: 2px;
}
.todo-item .done {
text-decoration: line-through;
color: #999;
}
</style>
进阶技巧:列表更新、过滤排序与v-for结合
列表渲染不仅是“展示”,更重要的是“交互”——增删改查、过滤排序是列表的常见需求。Vue提供了完善的方案,结合响应式数据和v-for,能轻松实现这些交互效果。
1. 列表更新:两种核心方式
Vue能够侦听响应式数组的变更,当数组发生变化时,会自动更新列表渲染。列表更新主要有两种方式,分别适用于不同场景:
- 变更方法:直接修改原数组,Vue会侦听这些方法的调用,触发列表更新,常用方法有push()、pop()、shift()、unshift()、splice()、sort()、reverse();
- 替换数组:不修改原数组,而是用新数组替代原数组,适用于需要保留原数组的场景,常用方法有filter()、concat()、slice()。
实战示例:实现todo列表的增删功能
<script setup>
import { ref } from 'vue';
const todos = ref([
{ id: 'todo-1', text: '学习Vue列表渲染', done: false },
{ id: 'todo-2', text: '掌握key的使用', done: true },
{ id: 'todo-3', text: '完成实战案例', done: false }
]);
const newTodoText = ref('');
// 添加todo(变更方法:push)
function addTodo() {
if (!newTodoText.value.trim()) return;
todos.value.push({
id: `todo-${Date.now()}`, // 用时间戳生成唯一id
text: newTodoText.value,
done: false
});
newTodoText.value = ''; // 清空输入框
}
// 删除todo(变更方法:splice)
function removeTodo(id) {
const index = todos.value.findIndex(todo => todo.id === id);
if (index !== -1) {
todos.value.splice(index, 1);
}
}
</script>
<template>
<div class="todo-list">
<h3>可增删的Todo列表</h3>
<div class="add-todo">
<input type="text" v-model="newTodoText" placeholder="请输入新的todo" />
<button @click="addTodo">添加</button>
</div>
<ul>
<li v-for="todo in todos" :key="todo.id" class="todo-item">
<input type="checkbox" v-model="todo.done" />
<span :class="{ done: todo.done }">{{ todo.text }}</span>
<button @click="removeTodo(todo.id)" class="delete-btn">删除</button>
</li>
</ul>
</div>
</template>
<style>
.todo-list {
margin: 20px 0;
padding: 15px;
border: 1px solid #eee;
border-radius: 4px;
}
.add-todo {
margin: 10px 0;
display: flex;
gap: 10px;
}
.add-todo input {
flex: 1;
padding: 6px;
border: 1px solid #ddd;
border-radius: 4px;
}
.add-todo button, .delete-btn {
padding: 6px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.add-todo button {
background: #42b983;
color: white;
}
.delete-btn {
background: #e53e3e;
color: white;
margin-left: 10px;
}
.todo-item {
margin: 8px 0;
list-style: none;
padding: 6px;
background: #f8f9fa;
border-radius: 2px;
display: flex;
align-items: center;
}
.todo-item .done {
text-decoration: line-through;
color: #999;
margin: 0 10px;
}
</style>
2. 过滤与排序:不修改原数组的列表渲染
有时我们需要展示数组过滤或排序后的结果,但又不想修改原数组(比如保留原始数据用于重置),此时最推荐使用计算属性,返回过滤或排序后的新数组,再用v-for渲染。
注意:在计算属性中使用sort()、reverse()时,要先创建原数组的副本,避免修改原数组(这两个方法会直接变更原数组)。
实战示例:实现todo列表的过滤与排序
<script setup>
import { ref, computed } from 'vue';
const todos = ref([
{ id: 'todo-1', text: '学习Vue列表渲染', done: false, time: 1713000000 },
{ id: 'todo-2', text: '掌握key的使用', done: true, time: 1713086400 },
{ id: 'todo-3', text: '完成实战案例', done: false, time: 1713172800 },
{ id: 'todo-4', text: '复习v-for知识点', done: false, time: 1713259200 }
]);
const filterType = ref('all'); // all: 全部, done: 已完成, undone: 未完成
// 计算属性:过滤todo列表
const filteredTodos = computed(() => {
let result = [...todos.value]; // 创建原数组副本,避免修改原数据
// 过滤逻辑
if (filterType.value === 'done') {
result = result.filter(todo => todo.done);
} else if (filterType.value === 'undone') {
result = result.filter(todo => !todo.done);
}
// 排序逻辑:按创建时间倒序(最新的在前面)
result.sort((a, b) => b.time - a.time);
return result;
});
</script>
<template>
<div class="todo-list">
<h3>可过滤排序的Todo列表</h3>
<div class="filter-buttons">
<button @click="filterType = 'all'" :class="{ active: filterType === 'all' }">全部</button>
<button @click="filterType = 'done'" :class="{ active: filterType === 'done' }">已完成</button>
<button @click="filterType = 'undone'" :class="{ active: filterType === 'undone' }">未完成</button>
</div>
<ul>
<li v-for="todo in filteredTodos" :key="todo.id" class="todo-item">
<input type="checkbox" v-model="todo.done" />
<span :class="{ done: todo.done }">{{ todo.text }}</span>
</li>
</ul>
</div>
</template>
<style>
.todo-list {
margin: 20px 0;
padding: 15px;
border: 1px solid #eee;
border-radius: 4px;
}
.filter-buttons {
margin: 10px 0;
display: flex;
gap: 10px;
}
.filter-buttons button {
padding: 6px 12px;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
background: #fff;
}
.filter-buttons button.active {
background: #42b983;
color: white;
border-color: #42b983;
}
.todo-item {
margin: 8px 0;
list-style: none;
padding: 6px;
background: #f8f9fa;
border-radius: 2px;
display: flex;
align-items: center;
}
.todo-item .done {
text-decoration: line-through;
color: #999;
margin: 0 10px;
}
</style>
避坑指南:v-for 与 v-if 的搭配禁忌
在之前的条件渲染博客中,我们简单提到过v-for与v-if的搭配问题,这里再详细拆解——当v-for和v-if同时存在于同一个元素上时,v-if的优先级高于v-for,这会导致v-if的条件无法访问到v-for作用域内的变量,引发渲染错误。
错误示例(不推荐)
<!-- 错误:v-if和v-for同元素,v-if无法访问todo变量 -->
<ul>
<li v-for="todo in todos" v-if="!todo.done" :key="todo.id">
{{ todo.text }}
</li>
</ul>
正确做法(推荐)
- 过滤列表:用计算属性过滤数组后,再用v-for渲染(最推荐),如上面的todo过滤示例;
- 整体控制:将v-if移至v-for的容器元素上,控制整个列表的显示/隐藏;
- 包裹模板:用template包裹v-for,将v-if写在内部元素上,确保v-if能访问到v-for的变量。
<!-- 正确做法3:用template包裹v-for -->
<ul>
<template v-for="todo in todos" :key="todo.id">
<li v-if="!todo.done">
{{ todo.text }}
</li>
</template>
</ul>
<!-- 正确做法2:v-if移至容器元素 -->
<ul v-if="todos.length > 0">
<li v-for="todo in todos" :key="todo.id">
{{ todo.text }}
</li>
</ul>
<div v-else>暂无数据</div>
扩展:组件上使用v-for
v-for不仅可以用于普通DOM元素,还可以直接用于Vue组件,实现组件列表的批量渲染。需要注意的是,组件有独立的作用域,v-for不会自动将迭代项传递给组件,必须通过props手动传递。
实战示例:组件化渲染商品列表
<!-- 子组件:GoodsItem.vue -->
<script setup>
const props = defineProps({
item: {
type: Object,
required: true
}
});
</script>
<template>
<div class="goods-item">
<h4>{{ item.name }}</h4>
<p>价格:¥{{ item.price }}</p>
<p>库存:{{ item.stock }}件</p>
</div>
</template>
<style>
.goods-item {
margin: 10px 0;
padding: 10px;
border: 1px solid #eee;
border-radius: 4px;
}
</style>
<!-- 父组件:GoodsList.vue -->
<script setup>
import { ref } from 'vue';
import GoodsItem from './GoodsItem.vue';
const goods = ref([
{ id: 1, name: 'Vue实战教程', price: 99, stock: 100 },
{ id: 2, name: 'JavaScript进阶指南', price: 89, stock: 50 },
{ id: 3, name: '前端面试题库', price: 69, stock: 30 }
]);
</script>
<template>
<div class="goods-list">
<h3>组件化商品列表</h3>
<!-- 在组件上使用v-for,通过props传递迭代项 -->
<GoodsItem
v-for="good in goods"
:key="good.id"
:item="good"
/>
</div>
</template>
总结:列表渲染的最佳实践
Vue的v-for指令为列表渲染提供了简洁高效的解决方案,从基础的数组遍历到复杂的组件列表,从简单展示到增删改查、过滤排序,几乎能覆盖所有列表场景。结合实战经验,提炼以下最佳实践,帮你避坑提效:
- 始终使用key:除了简单无状态的列表,务必为v-for添加唯一key,优先使用后端返回的id,避免使用索引;
- 合理选择遍历方式:数组遍历用“item in items”,对象遍历用“(value, key) in object”,固定数量元素用范围值;
- 列表更新选对方法:需要修改原数组用变更方法,需要保留原数组用替换方法;
- 过滤排序用计算属性:不修改原数组,且能缓存结果,提升性能;
- 避免v-for与v-if同元素:优先用计算属性过滤,或用template包裹分离二者;
- 组件列表传递props:在组件上使用v-for时,必须通过props传递迭代项,确保组件独立可复用。
列表渲染是Vue开发的基础,也是高频考点,掌握v-for的核心用法、key的原理以及各种进阶技巧,能让你在开发中更高效地处理列表需求,写出更健壮、可维护的前端代码。结合之前的条件渲染知识,二者搭配使用,能实现更复杂的页面交互效果,解锁更多Vue开发能力。