在 vue 中,使用 v-for 指令的时候,需要我们提供 key值,这个值的作用是用来标识当前 VNode 节点,在虚拟 dom diff 时会用到
vue 更新 Dom 节点一共会有三种操作 更新,插入,和删除,也就是移动 , 很显然,更新的效率是最高的,而删除和插入作用,会更改 dom 树,对性能影响最大。因此,我们在 vue 层面做性能优化时,主要就是尽可能的让 vue 减少插入,删除 dom 节点的操作
在虚拟 dom diff 时,key是判断两个 dom 节点的首要条件
在数组渲染时,不传 key 或者传入数组下标作为 key,在数组总长度不变的前提下,效果是一样的。因为不传 key 值时,key 是 undefined ,我们知道在 js 中 undefined === undefined
那么设置 key 和不设置 key 的区别是什么呢?在 vue 官网中,我们可以看到相关的说明,不设置 key 时,vue 会采取就地复用的策略
举个例子
`<div v-for="i in arr">{{ i }}</div>`
// 如果我们的数组是这样的
[1, 2, 3, 4, 5]
// 它的渲染结果是这样的
`<div>1</div>` // key: undefined
`<div>2</div>` // key: undefined
`<div>3</div>` // key: undefined
`<div>4</div>` // key: undefined
`<div>5</div>` // key: undefined
// 将它打乱
[4, 1, 3, 5, 2]
// 渲染结果是这样的 期间只发生了DOM节点的文本内容的更新
`<div>4</div>` // key: undefined
`<div>1</div>` // key: undefined
`<div>3</div>` // key: undefined
`<div>5</div>` // key: undefined
`<div>2</div>` // key: undefined
// 如果我们给这个数组每一项都设置了唯一的key
[{id: 'A', value: 1}, {id: 'B', value: 2}, {id: 'C', value: 3}, {id: 'D', value: 4}, {id: 'E', value: 5}]
// 它的渲染结果应该是这样的
`<div>1</div>` // key: A
`<div>2</div>` // key: B
`<div>3</div>` // key: C
`<div>4</div>` // key: D
`<div>5</div>` // key: E
// 将它打乱
[{id: 'D', value: 4}, {id: 'A', value: 1}, {id: 'C', value: 3}, {id: 'E', value: 5}, {id: 'B', value: 2}]
// 渲染结果是这样的 期间只发生了DOM节点的移动
`<div>4</div>` // key: D
`<div>1</div>` // key: A
`<div>3</div>` // key: C
`<div>5</div>` // key: E
`<div>2</div>` // key: B
tree diff 过程中,如果发现 key 相同,那么判断为同一个节点,不会进行移动操作,只会进行 dom 节点内容的更新,而不设置 key 或设置数组下标为 key,在数组长度没有发生变化时,可以看到每个节点永远是一样的,所以不会发生 dom 节点的移动。
可以看到,在这种场景下,不设置 key 而采用就地复用的策略,效率是比设置 key 高的。
那么如果是向数组中插入数据,而不是原地打乱数组顺序的情况呢
<body>
<div id="demo">
<p v-for="item in items" :key="item">{{item}}</p>
</div>
<script src="../../dist/vue.js"></script>
<script>
// 创建实例
const app = new Vue({
el: '#demo',
data: { items: ['a', 'b', 'c', 'd', 'e'] },
mounted () {
setTimeout(() => {
this.items.splice(2, 0, 'f') // 2 后插入数据
}, 2000);
},
});
</script>
</body>
在不使用key的情况,vue会进行这样的操作:
分析下整体流程:
- 比较A,A,相同类型的节点,进行patch,但数据相同,不发生dom操作
- 比较B,B,相同类型的节点,进行patch,但数据相同,不发生dom操作
- 比较C,F,相同类型的节点,进行patch,数据不同,发生dom操作
- 比较D,C,相同类型的节点,进行patch,数据不同,发生dom操作
- 比较E,D,相同类型的节点,进行patch,数据不同,发生dom操作
- 循环结束,将E插入到DOM中
一共发生了3次更新,1次插入操作
在使用key的情况:vue会进行这样的操作:
- 比较A,A,相同类型的节点,进行patch,但数据相同,不发生dom操作
- 比较B,B,相同类型的节点,进行patch,但数据相同,不发生dom操作
- 比较C,F,不相同类型的节点
-
- 比较E、E,相同类型的节点,进行patch,但数据相同,不发生dom操作
- 比较D、D,相同类型的节点,进行patch,但数据相同,不发生dom操作
- 比较C、C,相同类型的节点,进行patch,但数据相同,不发生dom操作
- 循环结束,将F插入到C之前
一共发生了0次更新,1次插入操作
可见设置key能够大大减少对页面的DOM操作,提高了diff效率
通过上面两种情况,可以看到设置 key是否能够提升效率,是不一定的,要分情况的。
那么什么情况采用就地复用情况更好呢,直接看 vue 官网的说明即可
这个默认的模式(就地复用)是高效的,但是只适用于不依赖子组件状态或临时 DOM 状态 (例如:表单输入值) 的列表渲染输出。
也就是说,子组件是无状态组件时,就地复用是更高效的,如果你不了解什么是无状态组件,也可以在 vue 官网中找到
比如笔者做过的一个项目中,需要展示 「协议链接列表」,每个链接是一个子组件,子组件的内容是一些依赖父组件状态的纯文本。为了提升性能,我将他们改为了函数式组件,也就是无状态组件,这个时候列表渲染就可以不传入 key,或者使用数组下标作 key
一般情况下,如果你不能明确的确定自己应当使用「就地复用」,那么你最好使用每个组件唯一的值作为 key值,以免造成性能浪费或引起一些 bug