Skip to content

列表渲染

v-for

我们可以使用 v-for 指令基于一个数组来渲染一个列表。v-for 指令的值需要使用 item in items 形式的特殊语法,其中 items 是源数据的数组,而 item 是迭代项的别名

js
const items = ref([{ message: "Foo" }, { message: "Bar" }]);
vue
<li v-for="item in items">
  {{ item.message }}
</li>

v-for 块中可以完整地访问父作用域内的属性和变量。v-for 也支持使用可选的第二个参数表示当前项的位置索引。

js
const parentMessage = ref("Parent");
const items = ref([{ message: "Foo" }, { message: "Bar" }]);
vue
<li v-for="(item, index) in items">
  {{ parentMessage }} - {{ index }} - {{ item.message }}
</li>

v-for 变量的作用域和下面的 JavaScript 代码很类似:

js
const parentMessage = "Parent";
const items = [
  /* ... */
];

items.forEach((item, index) => {
  // 可以访问外层的 `parentMessage`
  // 而 `item` 和 `index` 只在这个作用域可用
  console.log(parentMessage, item.message, index);
});

注意 v-for 是如何对应 forEach 回调的函数签名的。实际上,你也可以在定义 v-for 的变量别名时使用解构,和解构函数参数类似:

vue
<li v-for="{ message } in items">
  {{ message }}
</li>

<!-- 有 index 索引时 -->
<li v-for="({ message }, index) in items">
  {{ message }} {{ index }}
</li>

对于多层嵌套的 v-for,作用域的工作方式和函数的作用域很类似。每个 v-for 作用域都可以访问到父级作用域:

vue
<li v-for="item in items">
  <span v-for="childItem in item.children">
    {{ item.message }} {{ childItem }}
  </span>
</li>

你也可以使用 of 作为分隔符来替代 in,这更接近 JavaScript 的迭代器语法:

vue
<div v-for="item of items"></div>

v-for 与对象

你也可以使用 v-for 来遍历一个对象的所有属性。遍历的顺序会基于对该对象调用 Object.keys() 的返回值来决定。

js
const myObject = reactive({
  title: "How to do lists in Vue",
  author: "Jane Doe",
  publishedAt: "2016-04-10",
});
vue
<ul>
  <li v-for="value in myObject">
    {{ value }}
  </li>
</ul>

可以通过提供第二个参数表示属性名 (例如 key):

vue
<li v-for="(value, key) in myObject">
  {{ key }}: {{ value }}
</li>

第三个参数表示位置索引:

vue
<li v-for="(value, key, index) in myObject">
  {{ index }}. {{ key }}: {{ value }}
</li>

v-for 里使用范围值

v-for 可以直接接受一个整数值。在这种用例中,会将该模板基于 1...n 的取值范围重复多次。

vue
<span v-for="n in 10">{{ n }}</span>

注意此处 n 的初值是从 1 开始而非 0

<template> 上的 v-for

与模板上的 v-if 类似,你也可以在 <template> 标签上使用 v-for 来渲染一个包含多个元素的块。例如:

vue
<ul>
  <template v-for="item in items">
    <li>{{ item.msg }}</li>
    <li class="divider" role="presentation"></li>
  </template>
</ul>

v-forv-if

警告

同时使用 v-ifv-for 是不推荐的,因为这样二者的优先级不明显。

当它们同时存在于一个节点上时,v-ifv-for 的优先级更高。这意味着 v-if 的条件将无法访问到 v-for 作用域内定义的变量别名:

vue
<!--
 这会抛出一个错误,因为属性 todo 此时
 没有在该实例上定义
-->
<li v-for="todo in todos" v-if="!todo.isComplete">
  {{ todo.name }}
</li>

在外新包装一层 <vue> 再在其上使用 v-for 可以解决这个问题 (这也更加明显易读):

vue
<vue v-for="todo in todos">
  <li v-if="!todo.isComplete">
    {{ todo.name }}
  </li>
</vue>

通过 key 管理状态

Vue 默认按照“就地更新”的策略来更新通过 v-for 渲染的元素列表。当数据项的顺序改变时,Vue 不会随之移动 DOM 元素的顺序,而是就地更新每个元素,确保它们在原本指定的索引位置上渲染。

默认模式是高效的,但只适用于列表渲染输出的结果不依赖子组件状态或者临时 DOM 状态 (例如表单输入值) 的情况

为了给 Vue 一个提示,以便它可以跟踪每个节点的标识,从而重用和重新排序现有的元素,你需要为每个元素对应的块提供一个唯一的 key attribute:

vue
<div v-for="item in items" :key="item.id">
  <!-- 内容 -->
</div>

当你使用 <vue v-for> 时,key 应该被放置在这个 <vue> 容器上:

vue
<vue v-for="todo in todos" :key="todo.name">
  <li>{{ todo.name }}</li>
</vue>

注意

key 在这里是一个通过 v-bind 绑定的特殊 attribute。请不要和v-for 中使用对象里所提到的对象属性名相混淆。

推荐在任何可行的时候为 v-for 提供一个 key attribute,除非所迭代的 DOM 内容非常简单 (例如:不包含组件或有状态的 DOM 元素),或者你想有意采用默认行为来提高性能。

key 绑定的值期望是一个基础类型的值,例如字符串或 number 类型。不要用对象作为 v-for 的 key。关于 key attribute 的更多用途细节,请参阅 key API 文档

数组变化侦测

变更方法

Vue 能够侦听响应式数组的变更方法,并在它们被调用时触发相关的更新。这些变更方法包括:

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

替换一个数组

变更方法,顾名思义,就是会对调用它们的原数组进行变更。相对地,也有一些不可变 (immutable) 方法,例如 filter()concat()slice(),这些都不会更改原数组,而总是返回一个新数组。当遇到的是非变更方法时,我们需要将旧的数组替换为新的:

js
// `items` 是一个数组的 ref
items.value = items.value.filter((item) => item.message.match(/Foo/));

你可能认为这将导致 Vue 丢弃现有的 DOM 并重新渲染整个列表——幸运的是,情况并非如此。Vue 实现了一些巧妙的方法来最大化对 DOM 元素的重用,因此用另一个包含部分重叠对象的数组来做替换,仍会是一种非常高效的操作。

展示过滤或排序后的结果

有时,我们希望显示数组经过过滤或排序后的内容,而不实际变更或重置原始数据。在这种情况下,你可以创建返回已过滤或已排序数组的计算属性。

举例来说:

js
const numbers = ref([1, 2, 3, 4, 5]);

const evenNumbers = computed(() => {
  return numbers.value.filter((n) => n % 2 === 0);
});
vue
<li v-for="n in evenNumbers">{{ n }}</li>

在计算属性不可行的情况下 (例如在多层嵌套的 v-for 循环中),你可以使用以下方法:

js
const sets = ref([
  [1, 2, 3, 4, 5],
  [6, 7, 8, 9, 10],
]);

function even(numbers) {
  return numbers.filter((number) => number % 2 === 0);
}
vue
<ul v-for="numbers in sets">
  <li v-for="n in even(numbers)">{{ n }}</li>
</ul>

在计算属性中使用 reverse()sort() 的时候务必小心!这两个方法将变更原始数组,计算函数中不应该这么做。请在调用这些方法之前创建一个原数组的副本:

diff
- return numbers.reverse()
+ return [...numbers].reverse()

案例

v-for 列表渲染

目标:列表渲染,所在标签结构,按照数据数量,循环生成

  • 语法

    • v-for="(值,索引) in 目标结构"
    • v-for="值 in 目标结构"
  • 目标结构:

    • 可以遍历数组 / 对象 / 数字 / 字符串 (可遍历结构)
  • 注意: v-for 的临时变量名不能用到 v-for 范围外

vue
<script setup>
import { ref, reactive } from "vue";

const count = ref(10);
const arr = ref(["小明", "小欢欢", "大黄"]);
const stu_arr = reactive([
  {
    id: 1001,
    name: "孙悟空",
    sex: "男",
    hobby: "吃桃子",
  },
  {
    id: 1002,
    name: "猪八戒",
    sex: "男",
    hobby: "背媳妇",
  },
]);
const stu_obj = reactive({
  name: "小黑",
  age: 18,
  class: "1 期",
});
</script>

<template>
  <div>
    <!-- v-for 把一组数据,渲染成一组 DOM -->
    <!-- 口诀:让谁循环生成,v-for 就写谁身上 -->
    <!-- 语法 1:
      v-for="(值变量名,索引变量名) in 目标结构"
      口诀:想要谁循环就放到谁身上
     -->
    <ul>
      <li v-for="(item, index) in arr" :key="index">
        {{ item }} - {{ index }}
      </li>
    </ul>

    <!-- 语法 2:
      v-for="值变量名 in 目标结构"
      -->
    <ul>
      <li v-for="obj in stu_arr" :key="obj.id">
        <span>{{ obj.name }}</span> - <span>{{ obj.sex }}</span> -
        <span>{{ obj.hobby }}</span>
      </li>
    </ul>

    <!-- v-for 遍历对象 (了解) -->
    <!-- 语法 3:(了解)
      v-for="(value, key) in 对象"
       -->
    <div>
      <p v-for="(value, key) in stu_obj" :key="value">
        <span>{{ value }}</span>
        =======
        <span>{{ key }}</span>
      </p>
    </div>

    <!-- v-for 遍历整数 (了解) - 从 1 开始 -->
    <!-- 语法 4: (了解)
      v-for="变量名 in 固定数字"
      从 1 开始遍历
     -->
    <div v-for="n in count" :key="n">{{ n }}</div>
  </div>
</template>

<style scoped></style>

总结:vue 最常用指令,铺设页面利器,快速把数据赋予到相同的 dom 结构上循环生成

v-for 更新监测

目标:当 v-for 遍历的目标结构改变,Vue 触发 v-for 的更新

情况 1: 数组翻转

情况 2: 数组截取

情况 3: 更新值

口诀:

数组变更方法,就会导致 v-for 更新,页面更新

数组非变更方法,返回新数组,就不会导致 v-for 更新,可采用覆盖数组或 this.$set()

vue
<script setup>
import { ref, reactive } from "vue";

const arr = reactive([5, 3, 9, 2, 1]);
const reverse_arr = () => {
  // 1. 数组翻转可以让 v-for 更新
  arr.reverse();
};

const slice_arr = () => {
  // 2. 数组 slice 方法不会造成 v-for 更新
  // slice 不会改变原始数组
  // arr.slice(0, 3)

  // 解决 v-for 更新 - 覆盖原始数组
  const items = arr.slice(0, 3);
  arr.splice(0, arr.length);
  arr.push(...items);
};

const update_arr = () => {
  // 3. 更新某个值的时候
  arr[0] = 1000;
};
</script>

<vue>
  <div>
    <ul>
      <li v-for="(val, index) in arr" :key="index">
        {{ val }}
      </li>
    </ul>
    <button @click="reverse_arr">数组翻转</button>
    <button @click="slice_arr">截取前 3 个</button>
    <button @click="update_arr">更新第一个元素值</button>
  </div>
</vue>

<style scoped></style>

这些方法会触发数组改变,v-for 会监测到并更新页面

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

这些方法不会触发 v-for 更新

  • slice()
  • filter()
  • concat()

注意:vue 不能监测到数组里赋值的动作而更新,如果需要请使用 Vue.set() 或者 this.$set(), 或者覆盖整个数组

总结:改变原数组的方法才能让 v-for 更新