vue中v-for的索引与属性key


前言

这个东西算是一个毒瘤了。在 vue2 那会的编辑器都普遍建议在 v-for 里面写个唯一标识 key,否则就会警告提示。

但唯一标识这种东西,后端可能不会给我们传 id 过来。又或者前端只是渲染一个静态列表,懒得自己挨个列表项都加个 id 属性。所以至少笔者本人常年有着使用索引值作为 key 属性的恶劣习惯。

直到今天在阅读官网文档的时候偶然想起了这个问题,便尝试了解了下。一了解还真的吓一跳汗流浃背了吧

代码案例

现在有两个组件 app.vueTestComponent.vueapp.vue 中使用 v-for 对包含 TestComponent.vue 和一个删除按钮的内容做了循环渲染。
点击删除按钮时会删除当前项。内容分别如下:

案例运行预览地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<!-- app.vue -->
<script setup lang="ts">
import { ref } from "vue";
import TestComponent from "./TestComponent.vue";

const list = ref( [ "张三", "李四", "王五" ] );
function deleteRow( index: number ) {
list.value.splice( index, 1 );
}
</script>

<template>
<ul>
<li v-for="( item, index ) of list" :key="index">
<TestComponent />
<button @click="deleteRow( index )">删除</button>
</li>
</ul>
</template>

<!-- TestComponent.vue -->
<script setup lang="ts">
defineProps<{
name?: string;
}>();
</script>

<template>
<p>
<span>{{ name }}</span>
<span>{{ Math.floor( Math.random() * 100 ) }}</span>
</p>
</template>

解析

可以看到,在上面的例子中我们使用了 v-for 自带的 index 索引作为 key 的值。当我们尝试点击第二项的删除时,结果确是第三项被删除了。

这是因为在删除第二项后,原本第三项的 key2 变成了 1。这与删除之前的第二项的 key 是相同的。因此在 vue 眼里,第二行是一直存在的,不需要更新。依次类推,反而是最后一项消失了。

为什么平日遇不到这种异常

在平日开发中,我们均会向子组件传递相应的 props,像上面案例这样是基本无意义的。我们可以略微更改一下案例,使之符合我们日常的开发场景

1
2
<!-- app.vue -->
<TestComponent :name="item" />

在传递了 props 后,上面所说的情况依旧会发生。但在那之后又紧接着发生了连锁变化: 尽管第二行保持不变,但由于传递的 props 变为了下一行的内容。
因此第二行又在内容层面变为了第三行的内容,以此类推。最终看到的结果就像是第二行被删除了一样。

1
2
3
4
5
6
7
// 删除第二行前
张三5
李四43
王五32
// 删除第二行后
张三5
王五29

需要注意的是王五的随机数变了,这代表着王五这个组件被重新渲染了一下

v2 和 v3 有没有区别

答案是有的,我们可以对案例的内容再次修改一下(v2 是要使用 option 写法的,这里就不专门翻译了)

1
2
<!-- TestComponent.vue -->
<!--<span>{{ name }}</span>-->

我们将 props.name 使用的那行注释了,也就是传递过来的 props 并未被使用。此时我们再进行删除,就得到了截然不同的结果

  • v2: 依旧是最后一项被删除,删除顺序混乱
  • v3: 正常删除,第三行以及以后的随机数发生变化

这就说明了一点,在 v3 中,传入的 props 只要发生了变化,哪怕它未被使用,依旧会触发二次渲染。而 v2 只要不被使用则不会触发二次渲染。

结论

v-for 使用 index 作为 key 的值确实会出现问题,但其必须满足如下几个要求:

  • 被循环的内容必须为父子组件关系
  • 增删的操作必须在父组件中执行
  • 子组件没有依赖响应式的 props
  • 子组件并未使用响应式的 props(仅限 vue2)