v-for, ngFor, react(for) 列表真的需要key吗

总述问题

列表渲染时,在对列表插入和移除之类的操作时,会因为其设计的“替换算法”,导致语义上的对应映射错误

原始列表

1
2
3
[ ] A
[勾选] B
[ ] C

https://vuejs.org/v2/guide/list.html#Maintaining-State

https://forum.vuejs.org/t/v-for-with-simple-arrays-what-key-to-use/13692

https://www.jianshu.com/p/4bd5e745ce95

https://www.zhihu.com/question/61064119

实际效果(Vue/React)

删除B

1
2
[ ] A
[勾选] C

头部增加D

1
2
3
4
[ ] D
[勾选] A
[ ] B
[ ] C

期望效果(Ng)

删除B

1
2
[ ] A
[ ] C

头部增加D

1
2
3
4
[ ] D
[ ] A
[勾选] B
[ ] C

Vue

https://cromarmot.github.io/VueDemo/#/VueForDemo

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
34
35
36
37
38
39
40
<template>
<div>
<div>
<input v-model="name" type="text" />
<button @click="add">添加</button>
</div>
<div>With key</div>
<ul>
<li v-for="item in list" :key="item.id">
<input type="checkbox" /> {{ item.name }}
</li>
</ul>
<div>Without key</div>
<ul>
<li v-for="item in list"><input type="checkbox" /> {{ item.name }}</li>
</ul>
</div>
</template>

<script>
export default {
data() {
return {
name: '',
newId: 3,
list: [
{ id: 1, name: 'Name0' },
{ id: 2, name: 'Name1' },
{ id: 3, name: 'Name2' },
],
}
},
methods: {
add() {
this.list.unshift({ id: ++this.newId, name: this.name })
this.name = ''
},
},
}
</script>

React

Same with Vue, try it on https://codesandbox.io/

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import React, { Component } from "react";

export default class CharShop extends Component {
newId = 3;
constructor(props) {
super(props);
this.state = {
name: "",
list: [
{ id: 1, name: "Name0" },
{ id: 2, name: "Name1" },
{ id: 3, name: "Name2" }
]
};
}

add = () => {
this.state.list.unshift({ id: ++this.newId, name: this.state.name });
this.setState({ name: "", list: this.state.list });
};

handleChange = (e) => {
this.setState({ name: e.target.value });
};

render() {
return (
<div>
<div>
<input
value={this.state.name}
onChange={this.handleChange}
type="text"
/>
<button onClick={this.add}>添加</button>
</div>
<ul>
{this.state.list.map((item) => (
<li>
<input type="checkbox" /> {item.name}
</li>
))}
</ul>
</div>
);
}
}
1
2
3
4
5
Warning: Each child in a list should have a unique "key" prop.

Check the render method of `CharShop`. See https://reactjs.org/link/warning-keys for more information.
at li
at CharShop

Ng

https://cromarmot.github.io/NgDemo/#/for-demo

Smarter !?

1
2
3
4
5
6
7
8
<div>
<input [(ngModel)]="name" type="text" />
<button (click)="add()">添加</button>
</div>
<div>Without key</div>
<ul>
<li *ngFor="let item of list"><input type="checkbox" /> {{ item.name }}</li>
</ul>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import {Component} from '@angular/core';

@Component({
selector: 'app-for-demo',
templateUrl: './for-demo.component.html',
})
export class ForDemoComponent {
name = '';
newId = 3;
list = [
{id: 1, name: 'Name0'},
{id: 2, name: 'Name1'},
{id: 3, name: 'Name2'},
];

add(): void {
this.list.unshift({
id: ++this.newId,
name: this.name,
});
this.name = '';
}
}

总的来说就是一个 设计上导致的Bug

从代码语义上,VueReact在此时已经就不符合语义了,for出来的input元素内容渲染列表的内容是挂钩的。渲染为空(默认状态)都比现在这样的更正确。

从设计上讲,既然都想到了做值的ob拦截set/get,同样对于数组数据的每一项,可以用包裹的方式跟踪上,因为它们毕竟是会渲染到页面上的,从语义上 既然作为data(vue),state(react),class成员(ng)。

然而vue/react都没有

于是甩锅给用户,让用户提供key(虽然这部分主要是eslint在管理提示,但默认error,或者warn都会让用户难受),(而且在官方文档中,for的基础使用又是支持完全没有,虽然详细文档里有), ngtrackBy非一定要提供。

然而实际上,如果真的需要跟踪,因该有相应的数据(VM层面),怎么想都不会“复用错误”,比如上面的勾选,如果真的要用应该会有对应的勾选数组VM层进行跟踪

而还有不少是简单列表,非动态列表,key其实不必要的情况更多。

甚至产生不少用index作为key来解决eslint报错的代码,跟没写一样,单纯是为了不让eslint报错而写的代码

或者简单页面有两块列表时,要么塞额外字符串前后缀,要么再拆一个没啥卵用的组件层级来解决key冲突。

而又不建议用了 非数字和字符串的key,有些时候,对象的地址甚至也是在没有id时,更好的值

所以 直接关掉key的需求,每个列表的渲染有VM上的数据跟踪感觉更合理

key应该作为真的后台能提供唯一id时再使用的辅助功能

所以从实现上,Vue/React 也“不那么需要”修复这个Bug,使用者该做的是VM层有对应的数据跟踪,而不是打开必须key的lint配置,然后提供一个无意义/无语义的key