不只懂 Vue 语法:为何 v-for 的 key 必须是唯一值?v-for 与 v-if 能否同时使用?

问题回答

v-for 的 key 必须是唯一值,才可以让 Vue 在更新 v-for 所产生的列表时,能准确更新节点。相反,如果使用 index 作为 key,或者不绑定 key,Vue 就会以该节点的位置作为 key,有机会因为错误套用之前渲染过的节点而造成错误。

另外,Vue 官方不建议同时使用 v-forv-if,因为两者在执行上的优次不同,而且有机会浪费渲染效能。

以下会再作详情解说。

绑 index 就如没有绑 key 一样

关於 Vue 更新 v-for 的所产生的画面,有几个重点:

  • Vue 是采用「就地更新」来更新以 v-for 渲染的元素,并非移动 DOM 来完成。
  • Vue 会重用已经渲染的 DOM 节点,并利用该节点 key,对比旧节点与新节点的内容,来判断是否要更新该节点。

因为「重用节点」、「只更新需要更新的节点」这两个优势,所以很多人才说:绑 key 可以提升 v-for 的渲染效能。Vue 官方文件提到,如果没绑 key,就会用最小移动并且尽量原地修改的手法来更新资料。

但接下来的例子,会发现其实不论你是没有绑 key,还是只绑 index 当作 key。一样会出现同样问题。这次我以绑定 index 作为示范。例子是参考了这里的讨论再作调整。

先分享完整程序码:
https://codesandbox.io/s/todo-list-bang-index-zuo-wei-key-sfs9u?file=/src/App.vue

情况是:

  1. 我勾选了 "Buy dinner",该项目有移到已完成的区域
  2. 但未完成的 "Watch Netflix" 都被勾选了

对我们来说,待办列表由这样:

  • "Write blog"
  • "Buy dinner"
  • "Watch Netflix"

变成了:

  • "Write blog"
  • "Watch Netflix"

但 Vue 是使用遍历来检查每个节点。因此会做以下的事:

  • 第一个节点,"Writing blog" 没变,保留就好。
  • 第二个节点,"Buy dinner" 变成了 "Watch Netflix",因此把文字改成了 "Watch Netflix"。但第二个节点是存在的,只不过改了文字。因此原地重用第二个节点的画面,也就是显示勾选的 checkbox。
  • 第三个节点没了,直接销毁。

最可怕的是,虽然第二个节点的资料(Watch Netflix)被勾选,但它的 completed 其实是 false:

再沿用以上情况,最後我取消勾选在完成区域里的 "Buy dinner",会变成以下结果:

"Buy dinner" 仍然是勾选状态。原理同上,因为对 Vue 来说,第二个节点就是有勾选状态,你不过是换掉了文字内容,但第二个节点是存在的,并没有被移除,所以会重用第二个节点的画面。

解决方法:绑定唯一值的 key

要避免以上情况,就不能利用 index 来记录一个节点的画面状态,而是使用唯一值。当我们使用唯一值,Vue 官方文件说明会做以下的事:

key 特殊 attribute 主要用做 Vue 的虚拟 DOM 算法的提示,以在比对新旧节点组时辨识 VNodes。...使用 key 时,它会基于 key 的顺序变化重新排列元素,并且那些使用了已经不存在的 key 的元素将会被移除/销毁。

关於虚拟 DOM 的意思,可参考此系列的文章:
什麽是 Virtual DOM?Vue 如何利用 Virtual DOM?

换言之,如果现在我以 id 当作唯一值,并成为每个节点的 key。从Vue 的文档中,我们知道当 Vue 遍历节点时,会做两件事:

  • 把所有 id 的顺序记录起来作比对。
  • 用每个节点的 id 来比对旧节点与新节点。如果找不到该 id,就代表此元素已不存在,因此 Vue 会销毁此节点。

回到例子,这次问题就被解决了。

修改後的程序码:
https://codesandbox.io/s/todo-list-bang-id-zuo-wei-key-pfy1n?file=/src/App.vue

主要修改部分:

<li v-for="todo in incompletedList" :key="todo.id">
    <input type="checkbox" v-model="todo.completed" :id="todo.id" />
    <label :for="todo.id">{{ todo.title }}</label>
</li>

当我勾选了 "Buy dinner":

Vue 就会做以下的事:

  • 按 id 比对,例如比对 id: 1 的新旧节点,判断是否有变化,有的话就重新渲染作更新。即使 "Buy dinner" 被勾选了,勾选的画面只会套用在 id: 2 的节点上。因此 "Watch Netflix" 不会呈现勾选状态,因为它的 id 是 3。
  • 把现在的 id 次序与之前的作比较,以待办区域为例,旧的 id 次序是 1,2,3,现在是 1,3。Vue 就把在待办里,id: 2 的节点销毁,并建立 id: 3 的节点。

v-if 和 v-for 为什麽不能同时使用?

Vue 官方文件有提到,不建议同时使用v-ifv-for

注意:

  • Vue 2:v-for 优先於 v-if
  • Vue 3:v-if 优先於 v-for

因为其中一个语法会优先被执行,加上效能的问题。所以 Vue 官方不建议同时使用。

以 Vue 2 为例,意思是先跑 v-for 呈现每笔资料,之後每笔资料都会套用 v-if。如果有 1000 笔资料,只有 1 笔是因为 v-if 而不会被渲染,那麽就浪费了 999 个 v-if 的计算。

像以下官方例子:

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

每个 todo 都会被绑上 v-if,并计算是否要作显示。

建议做法

Vue 官方 style guide 建议两种常用做法:

  1. 先使用 computed 把资料处理好,再跑 v-for 渲染已处理好的资料。
  2. v-if 移到外层,内层用 v-for

第一种做法,之前的例子就有用到:

<ul>
  <li v-for="todo in completedList" :key="todo.id">
    <input type="checkbox" v-model="todo.completed" />
    {{ todo.title }}
  </li>
</ul>
  data() {
    return {
      todos: [
        {
          id: "1",
          title: "Write blog",
          completed: false,
        },
        ...
      ],
    };
  },
  computed: {
    completedList() {
      return this.todos.filter((item) => item.completed);
    },
  },

第二种做法在以上的例子就不能用,因为每个事项都有自己的完成状态,没法统一。

总结

  • 对於 v-for 渲染的画面,Vue 使用「原地更新」的方法来作更新,尽量重用已经渲染过的节点。
  • v-for 需要绑定 key, 因为 Vue 会判断该节点内容是否有变,以及会纪录所有 key 的顺序并作出更新。
  • key 必须要是唯一值,如果使用重复的值,可能会导致 Vue 错误套用节点的画面。
  • Vue 官方不建议同时使用 v-ifv-for,因为两者执行时有优次之分,而且有机会浪费渲染效能。

参考资料

vue中v-if和v-for不建议同时使用的坑
Vue2.0 v-for 中 :key 到底有什么用?
重新认识 Vue - 1-6 条件判断与列表渲染


<<:  Day26:Flow 的运算子 - buffer()

>>:  Day 16 - 透过Vuex来管理状态

GoDaddy 设定 DNS 转址到 IIS 上指定网站

当我们在 GoDaddy 上申请好网域之後,就接着要把 GoDaddy 上的 DNS 转址到我们的服...

Nutanix NCSC-Level-1 Dumps PDF with Actual NCSC-Level-1 Exam Questions

IT business is one of the most famous in the busin...

虹语岚访仲夏夜-21(专业的小四篇)

没有答案 也抓不住时间 独自成长 希望谁能发现 窗外阳光 季节又转一圈 遥远记忆 回不到的雨天 万...

Vue Router介绍

在昨天建置vue-cli插件时我们有新增vuex和vue-router,所以今天要先来介绍vue-r...

Day 24 | Service

Service是应用程序元件之一,它用於背景处理与使用者介面无关的长时间任务,即便切换到其他应用程序...