Skip to content

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(当列表会变化时)

下一步

接下来学习 表单处理,了解如何处理用户输入。