## 关于 Vue 和 React 中 key 的作用
### 什么是 key?
`React` 中的 key 属性,它是一个特殊的属性, 其实不只是`React`,`Vue`中在执行列表渲染时也会要求给每个组件添加上 key 这个属性。
怎么解释这个 `key` 这个玩意呢?这就要扯到 [虚拟 DOM 的 Diff 算法]()了,通过一套虚拟 DOM,使得我们不直接操作 DOM,而只需要操作数据就能够重新渲染页面。而隐藏在背后的原理便是其高效的[Diff 算法]()。
vue 和 react 的虚拟 DOM 的 Diff 算法大致相同,其核心是基于两个简单的假设:
- 两个相同的组件产生类似的 DOM 结构,不同的组件产生不同的 DOM 结构。
- 同一层级的一组节点,他们可以通过唯一的 id 进行区分。
当页面的数据发生变化时,Diff 算法只会比较同一层级的节点:
- 如果节点类型不同,直接干掉前面的节点,再创建并插入新的节点,不会再比较这个节点以后的子节点了。
- 如果节点类型相同,则会重新设置该节点的属性,从而实现节点的更新。
看到这里,你应该知道了吧,没错,这个唯一区分的 id 其实就是本文的重点 —— `key`
### key 的作用
以 React 举例,react 的作者之一 Paul O’Shannessy 有提到:
```javascript
Key is not really about performance, it’s more about identity (which in turn leads to better performance). Randomly assigned and changing values do not form an identity
```
简单来说,**react 利用 key 来识别组件,它是一种身份标识标识**,每个 key 对应一个组件,相同的 key react 认为是同一个组件,这样后续相同的 key 对应组件都不会被创建。
```javascript
import React from 'react'
import ReactDOM from 'react-dom'
class Test extends React.Components {
state = {
users: [
{
id: 1,
name: '董事长'
},
{
id:2,
name: '总经理'
},
{
id: 2,
name: "技术总监"
}
]
},
render()
return(
员工列表
{this.state.users.map(item =>
{item.id}:{item.name}
)}
)
)
}
```
上面代码在 dom 渲染挂载后,用户列表只有董事长和总经理两个用户,技术总监并没有展示处理,主要是因为 react 根据 key 认为总经理和技术总监是同一个组件,导致第一个被渲染,后续的会被丢弃掉。
这样,有了 key 属性后,就可以与组件建立了一种对应关系,react 根据 key 来决定是销毁重新创建组件还是更新组件。
- key 相同,若组件属性有所变化,则 react 只更新组件对应的属性;没有变化则不更新。
- key 值不同,则 react 先销毁该组件(有状态组件的 componentWillUnmount 会执行),然后重新创建该组件(有状态组件的 constructor 和 componentWillUnmount 都会执行)
> 注意 : key 不是用来提升 react 的性能的,不过用好 key 对性能是有帮组的。
用过 react 的开发者都知道,使用 React 的过程中,当组件的子元素是一系列类型相同元素时,就必须添加一个属性 key,否则 React 将给出一个 Warning,比如这样
```javascript
Warning: Each child in an array or iterator should have a unique "key" prop. Check the render method of `ServiceInfo`. See https://fb.me/react-warning-keys for more information.
```
哎呀,反正才 Warning 嘛,又不是 Error,加不加都无所谓啦,那么问题来啦,key 是不是必须的?其实是强制要求的,只不过 react 为按要求来默认上帮我们做了,它是以数组的 index 作为 key 的。
### 启发性算法
当组件的 props 和 state 发送改变时,React 都会调用 render 去重新渲染 UI,实质上 render 函数作用就是返回最新的元素树。目前存在大量的方法可以将一棵树转化成另一棵树,但它们的时间复杂度基本都是 O(n3),这么庞大的时间数量级我们是不能接受的,试想如果我们的组件返回的元素树中含有 100 个元素,那么一次一致性比较就要达到 1000000 的数量级,这显然是低效的,不可接受的。这时 React 就采用了启发式的算法。
React 启发式算法就是采用一系列前提和假设,使得比较前后元素树的时间复杂度由 O(n3)降低为 O(n),React 启发式算法的前提条件主要包括两点:
- 不同的两个元素会产生不同的树
- 可以使用 key 属性来表明不同的渲染中哪些元素是相同的
### 反模式
很多时候,我们可能并没有在遍历数组渲染组件的时候写上 key 的习惯,因为除了控制台报到一个 Warning,并不会有任何影响。因为赋 key 值这一步 react 帮我们做了,默认使用的是遍历过程中的 index 值。
```javascript
let arr = ['red', 'yellow'];
// 下边的 list1 和 list2 是等价的
const list1 = arr.map(item => {
return {item}
;
});
const list2 = arr.map((item, index) => {
return {item}
;
});
```
在上面的例子中,如果数组发生了变化,我们需要在数组的末尾插入一个元素 arr.push('black'),react 经过 diff 后就会发现 :key 值为 0 和 1 的元素并没有发生如何变化,所以 react 会认为, 最后需要在 UI 上发生变更,仅仅是插入一个 key 值为 2 的新元素。
但是如果我们在数组的开头插入了一个新元素 arr.unshift('green'),react 经过 diff 后就会发现每一个元素的 key 值都发生了变化,也是就说每个元素都要重新渲染一次,虽然从结果来看,仅仅是在开头添加了一个元素而已。如果负责渲染的数组数据量较大的话,则会对性能造成较大的影响。与 react 使用的启发式算法是相悖的。
因此,推荐的做法是每个兄弟元素都加上一个稳定唯一的 key 值。
### index 作为 key 的坑
先看段代码,
```html
{this.state.data.map((item,index) => return ) }
```
上面实例中在数组重新排序后, **key 对应的实例都没有销毁,而是重新更新**。具体更新过程我们拿 key=0 的元素来说明, 数组重新排序后:
- 组件重新 render 得到新的虚拟 dom;
- 新老两个虚拟 dom 进行 diff,新老版的都有 key=0 的组件,react 认为同一个组件,则只可能更新组件;
- 然后比较其 children,发现 key 相同,只是内容的文本内容不同(由 a--->c),而 input 组件并没有变化,这时触发组件的`componentWillReceiveProps`方法,从而更新其子组件文本内容;
- 因为组件的 children 中 input 组件没有变化,其又与父组件传入的 props 没有关联,所以 input 组件不会更新(即其 componentWillReceiveProps 方法不会被执行),导致用户输入的值不会变化。
> 这就是 index 作为 key 存在的问题,所以不要使用 index 作为 key。
### key 的值要保持唯一
在数组中生成的每项都要有 key 属性,并且 key 的值是一个永久且唯一的值,即稳定唯一。不能通过 `Math.random()` 来随机生成 key
### 友情链接
React 技术内幕 : https://juejin.im/post/59abb01c518825243f1b6dad
React 之 key 详解 : https://segmentfault.com/a/1190000009149186
个人博客 : https://github.com/PDKSophia/blog.io