Vue.js列表渲染

列表渲染是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指令

Vue的v-for指令提供了声明式的解决方案:

  • 声明式语法:使用简洁的模板语法描述列表结构
  • 自动响应:数据变化时,列表自动更新
  • 性能优化:Vue内部优化了列表渲染和更新
  • 多种数据源:支持数组、对象、数字范围等多种数据源
  • 完整功能:支持过滤、排序、动画等高级功能

v-for 基础用法

v-for指令基于源数据多次渲染元素或模板块,支持多种语法形式。

数据源
v-for 循环
生成DOM元素

1. 遍历数组

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>
                                
数组遍历演示
  • {{ index + 1 }}. {{ fruit.name }} {{ fruit.color }} ¥{{ fruit.price }}
{{ fruits }}

2. 遍历对象

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 }}

3. 遍历数字范围

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 -->
                                
数字范围遍历演示
{{ repeatCount }} 次

重复文本:

第 {{ n }} 次

乘法表:

× {{ n }}
{{ i }} {{ i * j }}

在 <template> 中使用 v-for

与条件渲染类似,可以在<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 元素 -->
                                
template 元素演示

用户列表:

不使用 <template>:
{{ user.name }}

{{ user.email }}

{{ user.role }}

注:每个用户卡片外有一个额外的div包裹元素

使用 <template>:

注:没有额外的包裹元素,DOM结构更简洁

{{ users }}

key 属性的重要性

当Vue更新使用v-for渲染的元素列表时,默认使用"就地更新"的策略。如果数据项的顺序被改变,Vue不会移动DOM元素来匹配数据项的顺序,而是就地更新每个元素。

为了给Vue一个提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,需要为每项提供一个唯一的key属性。

key属性演示

切换复选框,然后修改列表项内容,观察输入框状态

{{ todos }}
观察要点:
  • 不使用key: 修改输入框内容后,随机排序列表,输入框内容会"跟随"索引位置移动
  • 使用key: 修改输入框内容后,随机排序列表,输入框内容会"跟随"具体项移动
  • 性能影响: 使用key可以帮助Vue更高效地重用和重新排序元素
  • 状态保持: 使用key可以确保组件状态在列表重新排序时得到正确保持
key 属性的最佳实践
  • 总是使用key: 只要使用了v-for,就应该提供key属性
  • 使用唯一标识: 最好使用数据项本身的唯一标识,如id
  • 避免使用索引: 当列表顺序可能变化时,不要使用索引作为key
  • key必须稳定: key值在列表重新渲染时应保持不变
  • 不要使用随机数: 不要使用Math.random()这样的随机值作为key

数组更新检测

Vue对数组的变异方法进行了包装,所以它们也会触发视图更新。这些方法包括:

变异方法 (会触发视图更新)

// 以下方法会触发视图更新
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);
                                
数组更新演示
{{ index + 1 }}. {{ student.name }} - 分数: {{ student.score }} {{ student.score >= 60 ? '及格' : '不及格' }}
{{ pushCount }}
push()
{{ spliceCount }}
splice()
{{ sortCount }}
sort()
{{ filterCount }}
filter()
注意事项
  • 直接修改数组项: vm.items[0] = newValue 不会触发视图更新
  • 修改数组长度: vm.items.length = 0 不会触发视图更新
  • 解决方案: 使用 Vue.set(vm.items, index, newValue)vm.$set()
  • 对象数组: 修改对象属性会触发更新,因为Vue会深度观察对象

对象更新检测

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.set(): 新属性会立即显示,触发响应式更新
  • 删除属性: 需要使用Vue.delete()vm.$delete()
  • 已有属性: 修改已有属性的值会正常触发更新

显示过滤/排序后的结果

有时我们想要显示一个数组经过过滤或排序后的版本,而不实际改变或重置原始数据。在这种情况下,可以创建计算属性来返回过滤或排序后的数组。

过滤和排序演示
{{ product.name }}
{{ product.category }}
¥{{ product.price }}
库存: {{ product.stock }}
¥0 最高: ¥{{ maxPrice }} ¥5000

显示 {{ 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-forv-if处于同一节点时,v-for的优先级比v-if更高。这意味着v-if将分别重复运行于每个v-for循环中,这可能不是我们想要的。

情况 代码示例 问题 解决方案
v-if 在 v-for 内部

<li v-for="todo in todos" v-if="!todo.isComplete">
  {{ todo.text }}
</li>
                                            
v-if会在每个循环中执行,性能较差 使用计算属性过滤列表
v-for 在 v-if 内部

<ul v-if="todos.length">
  <li v-for="todo in todos">
    {{ todo.text }}
  </li>
</ul>
<p v-else>暂无待办事项</p>
                                            
合理用法,用于条件渲染整个列表 推荐用法,没有问题
v-for 和 v-if 在同一元素

<li v-for="todo in todos" v-if="shouldShowTodos">
  {{ todo.text }}
</li>
                                            
逻辑混乱,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>
                                
最佳实践
  • 避免在同一元素上使用v-for和v-if
  • 使用计算属性过滤列表:性能更好,逻辑更清晰
  • 在父元素上使用v-if:用于控制整个列表的显示/隐藏
  • 使用template元素包裹:如果需要同时使用,可以用template包裹
  • 考虑性能影响:大型列表中使用v-if过滤会导致性能问题

列表渲染的最佳实践

应该做的
  • 总是使用key:为v-for提供唯一的key属性
  • 使用计算属性过滤:避免在v-for中使用v-if过滤
  • 提取复杂逻辑:将复杂的列表逻辑提取到方法或计算属性中
  • 使用template包裹:渲染多个元素时使用template
  • 分页加载:大型列表使用分页或虚拟滚动
避免做的
  • 避免使用索引作为key:列表顺序变化时会导致问题
  • 避免在v-for中使用v-if:性能较差
  • 避免直接修改数组长度:使用变异方法或Vue.set
  • 避免在v-for中执行复杂计算:会导致重复计算
  • 避免渲染过多元素:大型列表影响性能
性能优化建议
  • 使用虚拟滚动:对于超大型列表(1000+项)
  • 懒加载:分批加载数据,不要一次加载所有
  • 使用Object.freeze:对于不会变化的大型列表
  • 避免不必要的响应式:对于纯展示数据,使用Object.freeze
  • 使用唯一key:帮助Vue高效更新DOM
  • 使用v-show替代v-if:对于频繁显示/隐藏的列表项

列表渲染综合演示

下面的演示展示了列表渲染的多种实际应用场景,包括任务管理、数据可视化、动态表单等。

任务管理系统

任务控制

任务统计:

总计: {{ tasks.length }} 个任务

进行中: {{ activeTaskCount }} 个

已完成: {{ completedTaskCount }} 个

完成率: {{ completionRate }}%

任务列表
暂无任务,请添加新任务
添加时间: {{ formatDate(task.addedAt) }} {{ getPriorityText(task.priority) }}
任务进度可视化
{{ completionRate }}% 完成
进行中: {{ activeTaskCount }} 已完成: {{ completedTaskCount }}
按优先级分布
{{ highPriorityCount }}

高优先级

{{ mediumPriorityCount }}

中优先级

{{ lowPriorityCount }}

低优先级

数据操作演示
核心列表渲染代码

// 任务数据
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指令用于基于数据源多次渲染元素,支持数组、对象和数字范围
  • 遍历数组v-for="(item, index) in items"
  • 遍历对象v-for="(value, key, index) in object"
  • key属性:为每项提供唯一key,帮助Vue高效更新DOM
  • 数组更新检测:使用变异方法或Vue.set()触发响应式更新
  • 对象更新检测:使用Vue.set()添加新属性,Vue.delete()删除属性
  • 过滤和排序:使用计算属性显示过滤或排序后的结果
  • v-for与v-if:避免在同一元素上使用,使用计算属性过滤
  • 最佳实践:总是使用key,避免性能陷阱,合理组织代码
  • 掌握列表渲染是构建动态数据驱动界面的关键技能

下一步:事件处理

现在你已经掌握了列表渲染,接下来我们将学习Vue的事件处理,包括如何监听DOM事件、使用事件修饰符、自定义事件等,使应用具备交互能力。