为什么要使用 key
?
Motivation
When you use React, at a single point in time you can think of the render() function as creating a tree of React elements. On the next state or props update, that render() function will return a different tree of React elements. React then needs to figure out how to efficiently update the UI to match the most recent tree.
译 :当您使用React时,您可以在单个时间点将该render()
函数视为创建React元素树。在下一个状态或道具更新时,该render()
函数将返回一个不同的React元素树。然后,React
需要弄清楚如何有效地更新UI以匹配最新的树。
这里涉及到两个算法复杂度的问题,总而言之就是:
- 不同类型的两个元素将产生不同的tree
- 开发人员可以通过
key
来暗示哪些子元素可以在不同的渲染中保持稳定
key 是如何工作的?
react中的key
属性是一个特殊的属性,它是出现不是给开发者用的(例如你为一个组件设置key
之后不能获取组件的这个key
props),而是给react自己用的。
react利用key
来识别组件,它是一种身份标识标识,就像我们的身份证用来辨识一个人一样。每个key
对应一个组件,相同的key
react认为是同一个组件,这样后续相同的key
对应组件都不会被创建。
this.state = {
users: [{id:1,name: '张三'}, {id:2, name: '李四'}, {id: 2, name: "王五"}],
....//省略
}
render()
return(
<div>
<h3>用户列表</h3>
{this.state.users.map(u => <div key={u.id}>{u.id}:{u.name}</div>)}
</div>
)
);
上面代码在dom
渲染挂载后,用户列表只有张三和李四两个用户,王五并没有展示处理,主要是因为react
根据key
认为李四和王五是同一个组件,导致第一个被渲染,后续的会被丢弃掉。
key的值必须保证唯一且稳定
这样,有了key
属性后,就可以与组件建立了一种对应关系,react
根据key
来决定是销毁重新创建组件还是更新组件。
-
key相同,若组件属性有所变化,则react只更新组件对应的属性;没有变化则不更新。
-
key
值不同,则react先销毁该组件(有状态组件的componentWillUnmount
会执行),然后重新创建该组件(有状态组件的constructor
和componentWillUnmount
都会执行)
还没完( 我再简单说两句 ),在项目开发中,key属性的使用场景最多的还是由数组动态创建的子组件的情况,需要为每个子组件添加唯一的key属性值。
index
的使用
在list数组中,用key
来标识数组创建子组件时,我的通常做法:
{this.state.data.map((v,idx) => <Item key={idx} v={v} />)}
// index 作为key方便快捷一步到位
<ul>
<li key="0">a <input type="text"/></li>
<li key="1">b <input type="text"/></li>
<li key="2">c <input type="text"/></li>
</ul>
但是···
若涉及到数组的动态变更,例如数组新增元素、删除元素或者重新排序等,这时index
作为key
会导致展示错误的数据。
{this.state.data.map((v,idx) => <Item key={idx} v={v} />)}
// 开始时:['a','b','c']=>
<ul>
<li key="0">a <input type="text"/></li>
<li key="1">b <input type="text"/></li>
<li key="2">c <input type="text"/></li>
</ul>
// 数组重排 -> ['c','b','a'] =>
<ul>
<li key="0">c <input type="text"/></li>
<li key="1">b <input type="text"/></li>
<li key="2">a <input type="text"/></li>
</ul>
上面实例中在数组重新排序后,key
对应的实例都没有销毁,而是重新更新。具体更新过程我们拿key=0
的元素来说明, 数组重新排序后:
-
组件重新
render
得到新的虚拟dom
; -
新老两个虚拟
dom
进行diff
,新老版的都有key=0
的组件,react
认为同一个组件,则只可能更新组件; -
然后比较其
children
,发现内容的文本内容不同(由a--->c),而input
组件并没有变化,这时触发组件的componentWillReceiveProps
方法,从而更新其子组件文本内容; -
因为组件的
children
中input
组件没有变化,其又与父组件传入的任props
没有关联,所以input
组件不会更新(即其componentWillReceiveProps
方法不会被执行),导致用户输入的值不会变化。
这就是index
作为key
存在的问题,index
作为key
是一种反模式, 不要轻易使用index作为key。(若数组的内容只是作为纯展示,而不涉及到数组的动态变更,还是很推荐的。)
官网的两个demo: demo1, demo2, 大家可以去看看。
index
的替代
归根结底,使用index
的问题在于两次渲染的index
是相同的,导致key
也是相同的,回到上面👆的总结 :key相同,若组件属性有所变化,则react只更新组件对应的属性;没有变化则不更新。
这时候,如果保证每次的 key
不同,问题不就解决了么?
于是乎···
key={index + Math.random()}
一行神奇的代码就产生了。
能解决问题么?能!是最优的么?不是。
翻看 官方文档 , 官方文档中明确指出 Don’t pass something like Math.random() to keys
。
key应该是稳定的,可预测的和独特的。不稳定的key
(如由其生成的key Math.random()
)将导致许多组件实例和DOM节点被不必要地重新创建,这可能导致性能下降和子组件中的丢失状态。
所以,在不能使用random
随机生成key
时,我们可以像下面这样用一个全局的localCounter
变量来添加稳定唯一的key
值。
var localCounter = 1;
this.data.forEach(el=>{
el.id = localCounter++;
});
//向数组中动态添加元素时,
function createUser(user) {
return {
...user,
id: localCounter++
}
}
所以,我最后的解决方案是全局定义一个变量:let ONE = 1;
,然后在组件中使用 key = {ONE++}
。这样 setSete()
的时候每次key
都产生了变化,也一定程度上避免了key
的不稳定性质。问题解决,收工。
补充一下问题场景:
当时遇到的问题是:需要改变的是内层组件的属性,而
key
设定在外层子组件上面。最开始使用index
作为key
, 两次渲染对外层子组件来说 index 是相同的,所以react 找到外层子组件识别到key = index
, 发现key
没变化。key
相同,若组件属性有所变化,则react
只更新组件对应的属性;没有变化则不更新。外层子组件上其实并没有属性改变,改变的属性是位于内层组件上。所以react
找到外层子组件就终止了。内层子组件相当于还是没渲染到。