Vue.js 列表渲染
v-for 指令
v-for 指令用于基于数组或对象渲染列表。
基本用法
vue
<script setup>
import { ref } from 'vue'
const items = ref(['苹果', '香蕉', '橙子'])
</script>
<template>
<ul>
<li v-for="item in items" :key="item">
{{ item }}
</li>
</ul>
</template>使用索引
vue
<script setup>
import { ref } from 'vue'
const items = ref(['苹果', '香蕉', '橙子'])
</script>
<template>
<ul>
<li v-for="(item, index) in items" :key="index">
{{ index + 1 }}. {{ item }}
</li>
</ul>
</template>遍历对象
vue
<script setup>
import { reactive } from 'vue'
const user = reactive({
name: '张三',
age: 25,
email: 'zhangsan@example.com'
})
</script>
<template>
<ul>
<li v-for="(value, key) in user" :key="key">
{{ key }}: {{ value }}
</li>
</ul>
</template>遍历对象数组
vue
<script setup>
import { ref } from 'vue'
const users = ref([
{ id: 1, name: '张三', age: 25 },
{ id: 2, name: '李四', age: 30 },
{ id: 3, name: '王五', age: 28 }
])
</script>
<template>
<table>
<thead>
<tr>
<th>ID</th>
<th>姓名</th>
<th>年龄</th>
</tr>
</thead>
<tbody>
<tr v-for="user in users" :key="user.id">
<td>{{ user.id }}</td>
<td>{{ user.name }}</td>
<td>{{ user.age }}</td>
</tr>
</tbody>
</table>
</template>key 属性的重要性
key 是 Vue 识别节点的通用机制,帮助 Vue 跟踪每个节点的身份。
为什么需要 key?
vue
<script setup>
import { ref } from 'vue'
const items = ref([
{ id: 1, text: '项目 1' },
{ id: 2, text: '项目 2' },
{ id: 3, text: '项目 3' }
])
function shuffle() {
items.value = items.value.sort(() => Math.random() - 0.5)
}
function remove(id) {
items.value = items.value.filter(item => item.id !== id)
}
</script>
<template>
<div>
<button @click="shuffle">打乱顺序</button>
<!-- ✅ 正确:使用唯一的 id 作为 key -->
<div v-for="item in items" :key="item.id">
{{ item.text }}
<button @click="remove(item.id)">删除</button>
</div>
</div>
</template>key 的最佳实践
vue
<!-- ❌ 不推荐:使用索引作为 key(当列表会变化时) -->
<div v-for="(item, index) in items" :key="index">
{{ item }}
</div>
<!-- ✅ 推荐:使用唯一标识符 -->
<div v-for="item in items" :key="item.id">
{{ item }}
</div>数组更新检测
Vue 能够检测以下数组变更方法:
push()pop()shift()unshift()splice()sort()reverse()
vue
<script setup>
import { ref } from 'vue'
const numbers = ref([1, 2, 3, 4, 5])
function addNumber() {
numbers.value.push(numbers.value.length + 1)
}
function removeFirst() {
numbers.value.shift()
}
function removeLast() {
numbers.value.pop()
}
function sortNumbers() {
numbers.value.sort((a, b) => b - a)
}
</script>
<template>
<div>
<p>数字列表:{{ numbers }}</p>
<button @click="addNumber">添加</button>
<button @click="removeFirst">删除第一个</button>
<button @click="removeLast">删除最后一个</button>
<button @click="sortNumbers">降序排序</button>
</div>
</template>过滤和排序
使用计算属性来过滤或排序列表:
vue
<script setup>
import { ref, computed } from 'vue'
const numbers = ref([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
const filterType = ref('all') // 'all', 'even', 'odd'
const filteredNumbers = computed(() => {
if (filterType.value === 'even') {
return numbers.value.filter(n => n % 2 === 0)
} else if (filterType.value === 'odd') {
return numbers.value.filter(n => n % 2 !== 0)
}
return numbers.value
})
</script>
<template>
<div>
<button @click="filterType = 'all'">全部</button>
<button @click="filterType = 'even'">偶数</button>
<button @click="filterType = 'odd'">奇数</button>
<ul>
<li v-for="num in filteredNumbers" :key="num">
{{ num }}
</li>
</ul>
</div>
</template>实战示例:待办事项列表
vue
<script setup>
import { ref, computed } from 'vue'
const newTodo = ref('')
const todos = ref([
{ id: 1, text: '学习 Vue', done: false },
{ id: 2, text: '写代码', done: false },
{ id: 3, text: '看书', done: true }
])
const filter = ref('all') // 'all', 'active', 'completed'
const filteredTodos = computed(() => {
if (filter.value === 'active') {
return todos.value.filter(todo => !todo.done)
} else if (filter.value === 'completed') {
return todos.value.filter(todo => todo.done)
}
return todos.value
})
const stats = computed(() => ({
total: todos.value.length,
active: todos.value.filter(t => !t.done).length,
completed: todos.value.filter(t => t.done).length
}))
function addTodo() {
if (newTodo.value.trim()) {
todos.value.push({
id: Date.now(),
text: newTodo.value,
done: false
})
newTodo.value = ''
}
}
function removeTodo(id) {
todos.value = todos.value.filter(todo => todo.id !== id)
}
function toggleTodo(id) {
const todo = todos.value.find(t => t.id === id)
if (todo) {
todo.done = !todo.done
}
}
</script>
<template>
<div class="todo-app">
<h2>待办事项</h2>
<!-- 添加新任务 -->
<div class="add-todo">
<input
v-model="newTodo"
@keyup.enter="addTodo"
placeholder="添加新任务..."
/>
<button @click="addTodo">添加</button>
</div>
<!-- 过滤器 -->
<div class="filters">
<button
:class="{ active: filter === 'all' }"
@click="filter = 'all'"
>
全部 ({{ stats.total }})
</button>
<button
:class="{ active: filter === 'active' }"
@click="filter = 'active'"
>
进行中 ({{ stats.active }})
</button>
<button
:class="{ active: filter === 'completed' }"
@click="filter = 'completed'"
>
已完成 ({{ stats.completed }})
</button>
</div>
<!-- 任务列表 -->
<ul class="todo-list">
<li
v-for="todo in filteredTodos"
:key="todo.id"
:class="{ completed: todo.done }"
>
<input
type="checkbox"
:checked="todo.done"
@change="toggleTodo(todo.id)"
/>
<span>{{ todo.text }}</span>
<button @click="removeTodo(todo.id)" class="delete-btn">删除</button>
</li>
</ul>
<p v-if="filteredTodos.length === 0" class="empty">
没有任务
</p>
</div>
</template>
<style scoped>
.todo-app {
max-width: 600px;
margin: 20px auto;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
}
.add-todo {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.add-todo input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
.add-todo button {
padding: 10px 20px;
background-color: #42b983;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.filters {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.filters button {
padding: 8px 16px;
background-color: #f5f5f5;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
}
.filters button.active {
background-color: #42b983;
color: white;
border-color: #42b983;
}
.todo-list {
list-style: none;
padding: 0;
}
.todo-list li {
display: flex;
align-items: center;
gap: 10px;
padding: 10px;
border-bottom: 1px solid #eee;
}
.todo-list li.completed span {
text-decoration: line-through;
color: #999;
}
.todo-list li span {
flex: 1;
}
.delete-btn {
padding: 4px 12px;
background-color: #f56c6c;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.empty {
text-align: center;
color: #999;
padding: 40px;
}
</style>实战示例:商品列表
vue
<script setup>
import { ref, computed } from 'vue'
const products = ref([
{ id: 1, name: 'iPhone 14', price: 5999, category: '手机', stock: 10 },
{ id: 2, name: 'MacBook Pro', price: 12999, category: '电脑', stock: 5 },
{ id: 3, name: 'iPad Air', price: 4399, category: '平板', stock: 8 },
{ id: 4, name: 'AirPods Pro', price: 1999, category: '耳机', stock: 15 },
{ id: 5, name: 'Apple Watch', price: 2999, category: '手表', stock: 0 }
])
const searchQuery = ref('')
const selectedCategory = ref('all')
const sortBy = ref('name') // 'name', 'price', 'stock'
const categories = computed(() => {
const cats = new Set(products.value.map(p => p.category))
return ['all', ...cats]
})
const filteredProducts = computed(() => {
let result = products.value
// 按分类过滤
if (selectedCategory.value !== 'all') {
result = result.filter(p => p.category === selectedCategory.value)
}
// 按搜索词过滤
if (searchQuery.value) {
result = result.filter(p =>
p.name.toLowerCase().includes(searchQuery.value.toLowerCase())
)
}
// 排序
result = [...result].sort((a, b) => {
if (sortBy.value === 'price') {
return a.price - b.price
} else if (sortBy.value === 'stock') {
return b.stock - a.stock
}
return a.name.localeCompare(b.name)
})
return result
})
</script>
<template>
<div class="product-list">
<h2>商品列表</h2>
<!-- 搜索和过滤 -->
<div class="controls">
<input
v-model="searchQuery"
placeholder="搜索商品..."
class="search-input"
/>
<select v-model="selectedCategory">
<option v-for="cat in categories" :key="cat" :value="cat">
{{ cat === 'all' ? '全部分类' : cat }}
</option>
</select>
<select v-model="sortBy">
<option value="name">按名称排序</option>
<option value="price">按价格排序</option>
<option value="stock">按库存排序</option>
</select>
</div>
<!-- 商品网格 -->
<div class="products-grid">
<div
v-for="product in filteredProducts"
:key="product.id"
class="product-card"
:class="{ 'out-of-stock': product.stock === 0 }"
>
<h3>{{ product.name }}</h3>
<p class="category">{{ product.category }}</p>
<p class="price">¥{{ product.price }}</p>
<p class="stock">
库存:{{ product.stock }}
<span v-if="product.stock === 0" class="badge">缺货</span>
</p>
<button :disabled="product.stock === 0">
{{ product.stock === 0 ? '缺货' : '加入购物车' }}
</button>
</div>
</div>
<p v-if="filteredProducts.length === 0" class="no-results">
没有找到商品
</p>
</div>
</template>
<style scoped>
.product-list {
max-width: 1200px;
margin: 20px auto;
padding: 20px;
}
.controls {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.search-input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
select {
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
.products-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
}
.product-card {
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
transition: transform 0.2s;
}
.product-card:hover {
transform: translateY(-5px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.product-card.out-of-stock {
opacity: 0.6;
}
.category {
color: #666;
font-size: 14px;
}
.price {
font-size: 24px;
font-weight: bold;
color: #42b983;
margin: 10px 0;
}
.stock {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.badge {
background-color: #f56c6c;
color: white;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
}
button {
width: 100%;
padding: 10px;
background-color: #42b983;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.no-results {
text-align: center;
color: #999;
padding: 40px;
}
</style>总结
- 使用
v-for渲染列表 - 始终提供唯一的
key属性 - 使用计算属性进行过滤和排序
- Vue 能检测数组的变更方法
- 避免使用索引作为 key(当列表会变化时)
下一步
接下来学习 表单处理,了解如何处理用户输入。