列表渲染是Vue.js中一个核心功能,用于基于数据源动态生成多个重复的DOM元素。通过v-for指令,我们可以轻松地遍历数组、对象和数字范围,创建动态列表、表格和其他重复的结构。
列表渲染不仅简化了数据到视图的映射,还提供了强大的功能如过滤、排序、动画过渡等,使得处理动态数据集合变得异常简单和高效。
在Web应用中,经常需要展示数据列表,如商品列表、用户列表、新闻列表等。传统的手动创建每个元素的方式存在诸多问题:
手动为每个数据项编写相似的HTML结构,导致大量重复代码。
<ul>
<li>商品1</li>
<li>商品2</li>
<li>商品3</li>
<!-- ... 更多重复代码 ... -->
</ul>
数据变化时需要手动更新DOM,容易出错且难以维护。
// 手动添加新项
const list = document.getElementById('list');
const newItem = document.createElement('li');
newItem.textContent = '新商品';
list.appendChild(newItem);
手动操作DOM可能导致性能问题,特别是对于大型列表。
// 清空并重新渲染整个列表
list.innerHTML = '';
data.forEach(item => {
const li = document.createElement('li');
li.textContent = item.name;
list.appendChild(li);
});
数据变化时不会自动更新视图,需要手动同步。
// 需要手动监听数据变化
dataSource.on('change', updateList);
function updateList() {
// 手动更新DOM
}
Vue的v-for指令提供了声明式的解决方案:
v-for指令基于源数据多次渲染元素或模板块,支持多种语法形式。
v-for最常见的用法是遍历数组,可以访问每个项和其索引。
<!-- 基本语法:item in items -->
<ul>
<li v-for="item in items">
{{ item }}
</li>
</ul>
<!-- 带索引:第二个参数是索引 -->
<ul>
<li v-for="(item, index) in items">
{{ index }}: {{ item }}
</li>
</ul>
<!-- 使用 of 替代 in,效果相同 -->
<ul>
<li v-for="item of items">
{{ item }}
</li>
</ul>
{{ fruits }}
v-for也可以遍历对象的属性,可以访问属性值、键名和索引。
<!-- 遍历对象属性值 -->
<ul>
<li v-for="value in object">
{{ value }}
</li>
</ul>
<!-- 遍历属性值和键名 -->
<ul>
<li v-for="(value, key) in object">
{{ key }}: {{ value }}
</li>
</ul>
<!-- 遍历属性值、键名和索引 -->
<ul>
<li v-for="(value, key, index) in object">
{{ index }}. {{ key }}: {{ value }}
</li>
</ul>
| 序号 | 属性名 | 属性值 | 数据类型 |
|---|---|---|---|
| {{ index + 1 }} | {{ key }} |
{{ value }} | {{ typeof value }} |
{{ user }}
v-for也可以接受整数,它会基于数字范围重复多次模板。
<!-- 遍历数字范围 -->
<span v-for="n in 10">{{ n }} </span>
<!-- 注意:从1开始,不是0 -->
<span v-for="n in 5">{{ n }} </span>
<!-- 输出: 1 2 3 4 5 -->
<!-- 结合索引使用 -->
<span v-for="(n, index) in 5">{{ index }}:{{ n }} </span>
<!-- 输出: 0:1 1:2 2:3 3:4 4:5 -->
重复文本:
乘法表:
| × | {{ n }} |
|---|---|
| {{ i }} | {{ i * j }} |
与条件渲染类似,可以在<template>元素上使用v-for来渲染多个元素块,<template>本身不会被渲染。
<!-- 使用 template 渲染多个元素 -->
<template v-for="item in items">
<li>{{ item.name }}</li>
<li>{{ item.price }}</li>
</template>
<!-- 对比:不使用 template -->
<div v-for="item in items">
<li>{{ item.name }}</li>
<li>{{ item.price }}</li>
</div>
<!-- 会多出一个包裹的 div 元素 -->
用户列表:
{{ user.email }}
{{ user.role }}
注:每个用户卡片外有一个额外的div包裹元素
{{ user.email }}
{{ user.role }}
注:没有额外的包裹元素,DOM结构更简洁
{{ users }}
当Vue更新使用v-for渲染的元素列表时,默认使用"就地更新"的策略。如果数据项的顺序被改变,Vue不会移动DOM元素来匹配数据项的顺序,而是就地更新每个元素。
为了给Vue一个提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,需要为每项提供一个唯一的key属性。
切换复选框,然后修改列表项内容,观察输入框状态
{{ todos }}
v-for,就应该提供key属性idMath.random()这样的随机值作为keyVue对数组的变异方法进行了包装,所以它们也会触发视图更新。这些方法包括:
// 以下方法会触发视图更新
push() // 向数组末尾添加元素
pop() // 删除数组最后一个元素
shift() // 删除数组第一个元素
unshift() // 向数组开头添加元素
splice() // 添加/删除/替换元素
sort() // 排序数组
reverse() // 反转数组
// 使用示例
vm.items.push({ message: '新项' });
vm.items.splice(0, 1); // 删除第一个元素
// 以下方法不会触发视图更新
filter() // 过滤数组
concat() // 连接数组
slice() // 截取数组
// 这些方法会返回新数组,需要重新赋值
vm.items = vm.items.filter(item => item.message.match(/Foo/));
vm.items = vm.items.concat([{ message: '新项' }]);
vm.items = vm.items.slice(1, 3);
vm.items[0] = newValue 不会触发视图更新vm.items.length = 0 不会触发视图更新Vue.set(vm.items, index, newValue) 或 vm.$set()Vue不能检测到对象属性的添加或删除,需要使用Vue.set()方法或vm.$set()实例方法。
// Vue 不能检测以下对象更新
vm.obj.newProp = 'hi'; // 不会触发更新
// 使用 Vue.set 或 vm.$set
Vue.set(vm.obj, 'newProp', 'hi');
// 或
vm.$set(vm.obj, 'newProp', 'hi');
// 添加多个新属性
vm.obj = Object.assign({}, vm.obj, {
newProp1: 'hi',
newProp2: 'hello'
});
// 删除属性 (需要使用 Vue.delete 或 vm.$delete)
Vue.delete(vm.obj, 'oldProp');
// 或
vm.$delete(vm.obj, 'oldProp');
| 属性名 | 属性值 | 操作 |
|---|---|---|
{{ key }} |
{{ value }} |
{{ product }}
Vue.delete()或vm.$delete()有时我们想要显示一个数组经过过滤或排序后的版本,而不实际改变或重置原始数据。在这种情况下,可以创建计算属性来返回过滤或排序后的数组。
显示 {{ filteredAndSortedProducts.length }} / {{ products.length }} 个商品
搜索关键词: "{{ searchKeyword }}"
筛选类别: {{ selectedCategory }}
排序方式: {{ getSortDescription() }}
computed: {
// 获取所有不重复的类别
categories() {
return [...new Set(this.products.map(p => p.category))];
},
// 过滤和排序商品
filteredAndSortedProducts() {
let result = this.products;
// 按关键词过滤
if (this.searchKeyword) {
const keyword = this.searchKeyword.toLowerCase();
result = result.filter(p =>
p.name.toLowerCase().includes(keyword) ||
p.category.toLowerCase().includes(keyword)
);
}
// 按类别过滤
if (this.selectedCategory) {
result = result.filter(p => p.category === this.selectedCategory);
}
// 按价格过滤
result = result.filter(p => p.price <= this.maxPrice);
// 按库存过滤
if (this.showLowStock) {
result = result.filter(p => p.stock < 10);
}
// 排序
switch (this.sortBy) {
case 'name':
result = result.sort((a, b) => a.name.localeCompare(b.name));
break;
case 'price-asc':
result = result.sort((a, b) => a.price - b.price);
break;
case 'price-desc':
result = result.sort((a, b) => b.price - a.price);
break;
case 'stock':
result = result.sort((a, b) => b.stock - a.stock);
break;
}
return result;
}
}
当v-for和v-if处于同一节点时,v-for的优先级比v-if更高。这意味着v-if将分别重复运行于每个v-for循环中,这可能不是我们想要的。
| 情况 | 代码示例 | 问题 | 解决方案 |
|---|---|---|---|
| v-if 在 v-for 内部 |
|
v-if会在每个循环中执行,性能较差 | 使用计算属性过滤列表 |
| v-for 在 v-if 内部 |
|
合理用法,用于条件渲染整个列表 | 推荐用法,没有问题 |
| v-for 和 v-if 在同一元素 |
|
逻辑混乱,shouldShowTodos会被重复判断 | 将v-if移到父元素 |
// 不好的用法:v-if 在 v-for 内部
<ul>
<li v-for="user in users" v-if="user.isActive">
{{ user.name }}
</li>
</ul>
// 好的用法1:使用计算属性过滤
computed: {
activeUsers() {
return this.users.filter(user => user.isActive);
}
}
<ul>
<li v-for="user in activeUsers">
{{ user.name }}
</li>
</ul>
// 好的用法2:在父元素上使用 v-if
<ul v-if="users.length">
<li v-for="user in users">
{{ user.name }}
</li>
</ul>
<p v-else>暂无用户</p>
// 好的用法3:使用 template 包裹
<template v-for="user in users">
<li v-if="user.isActive" :key="user.id">
{{ user.name }}
</li>
</template>
下面的演示展示了列表渲染的多种实际应用场景,包括任务管理、数据可视化、动态表单等。
任务统计:
总计: {{ tasks.length }} 个任务
进行中: {{ activeTaskCount }} 个
已完成: {{ completedTaskCount }} 个
完成率: {{ completionRate }}%
高优先级
中优先级
低优先级
// 任务数据
data() {
return {
tasks: [
{ id: 1, text: '学习Vue列表渲染', completed: true, priority: 'high', addedAt: new Date() },
{ id: 2, text: '完成项目开发', completed: false, priority: 'high', addedAt: new Date() },
{ id: 3, text: '整理文档', completed: false, priority: 'medium', addedAt: new Date() },
{ id: 4, text: '代码审查', completed: true, priority: 'low', addedAt: new Date() }
],
newTask: { text: '', priority: 'medium' },
taskFilter: 'all',
taskSort: 'added'
};
},
// 计算属性
computed: {
// 过滤和排序任务
filteredAndSortedTasks() {
let result = this.tasks;
// 过滤
switch (this.taskFilter) {
case 'active':
result = result.filter(t => !t.completed);
break;
case 'completed':
result = result.filter(t => t.completed);
break;
case 'high':
result = result.filter(t => t.priority === 'high');
break;
}
// 排序
switch (this.taskSort) {
case 'priority':
const priorityOrder = { high: 3, medium: 2, low: 1 };
result = result.sort((a, b) => priorityOrder[b.priority] - priorityOrder[a.priority]);
break;
case 'text':
result = result.sort((a, b) => a.text.localeCompare(b.text));
break;
case 'added':
default:
result = result.sort((a, b) => b.addedAt - a.addedAt);
break;
}
return result;
},
// 统计信息
activeTaskCount() {
return this.tasks.filter(t => !t.completed).length;
},
completedTaskCount() {
return this.tasks.filter(t => t.completed).length;
},
completionRate() {
if (this.tasks.length === 0) return 0;
return Math.round((this.completedTaskCount / this.tasks.length) * 100);
},
// 按优先级统计
highPriorityCount() {
return this.tasks.filter(t => t.priority === 'high').length;
},
mediumPriorityCount() {
return this.tasks.filter(t => t.priority === 'medium').length;
},
lowPriorityCount() {
return this.tasks.filter(t => t.priority === 'low').length;
}
},
// 方法
methods: {
addTask() {
if (!this.newTask.text.trim()) return;
this.tasks.push({
id: Date.now(),
text: this.newTask.text,
completed: false,
priority: this.newTask.priority,
addedAt: new Date()
});
this.newTask.text = '';
this.newTask.priority = 'medium';
},
deleteTask(id) {
const index = this.tasks.findIndex(t => t.id === id);
if (index !== -1) {
this.tasks.splice(index, 1);
}
}
}
v-for="(item, index) in items"v-for="(value, key, index) in object"现在你已经掌握了列表渲染,接下来我们将学习Vue的事件处理,包括如何监听DOM事件、使用事件修饰符、自定义事件等,使应用具备交互能力。