react中为何推荐设置key
关于react渲染原理的相关知识,可参考我的另一篇文章preact源码解析,从preact中理解react原理
我想很多人刚开始使用react框架的时候,和我刚开始一样迷惘。只知道 map 返回的元素项推荐设置key,却不知为何要设置key。当初我也在网上找了一些相关的文章,却看得一塌糊涂,始终没明白为啥要设置key。后来研究了preact源码,才恍然大悟。
设置key可以帮助react识别那个项被修改、添加或者移除。我们写的jsx语法,首先在项目打包阶段转换为js语法,然后在浏览器执行后会是一个虚拟节点树,最后调用react-dom的render方法渲染成页面中的真实dom树。用示例说明:
//react代码
ReactDom.render(<div>
{[1, 2].map(item => <span>{item}</span>)}
</div>, document.getElementById('app'));
//首先babel编译替换
ReactDom.render(React.createElement(
"div",
null,
[1, 2].map(function (item) {
return React.createElement("span", null, item);
})
), document.getElementById('app'));
//然后先执行render的参数(这儿省略了虚拟节点的其它属性)
ReactDom.render({
type: 'div', key: null, props: {
children: [
{
type: 'span', key: null, props: {
children: 1
}
},
{
type: 'span', key: null, props: {
children: 2
}
}
]
}
}, document.getElementById('app'));
//最后执行render方法渲染为真实dom树
<div>
<span>1</span>
<span>2</span>
</div>
首次渲染时,只有新虚拟节点,没有老虚拟节点,因此不会涉及到新老节点的对比(常说的diff)。而当页面渲染完成后,单击某个组件会调用setState方法,引发它的状态改变。这时会开始渲染这个组件,执行组件的render方法,并返回新的虚拟节点树。假如以上示例中的[1,2]数组数据,如果单击了删除按钮,删除了数组的第一个项后变为[2],我们来分析下新的虚拟节点树与之前的虚拟节点树渲染对比。此时新的虚拟节树点为:
{
type: 'div', key: null, props: {
children: [
{
type: 'span', key: null, props: {
children: 2
}
}
]
}
}
- 首先开始渲染div虚拟节点。从之前虚拟节点树的同辈节点中遍历查找老的虚拟节点,遍历的过程中会不断对比。在与之前第一个div虚拟节点对比中,由于type和key都与新虚拟节点的相同(key都为null),于是会把这个虚拟节点作为新div虚拟节点对应的老节点。如果存在老节点,假如他是字符串类型的节点,会储存着上次渲染的真实dom节点,假如是函数类型的节点,会储存着之前组件的实例,这样就不会再去实例一个组件。由于这儿直接发现了老节点,所以会直接读取老节点储存的真实dom节点作为新div虚拟节点的dom节点,也就是直接使用之前页面中已经存在的div这个节点,这样就不会重新创建一个div类型的dom节点。
- 接下来开始比较虚拟节点div的子节点。从第一个虚拟节点span开始,先查找之前对应的老虚拟节点。由于之前第一个span虚拟节点的type与key和新的span虚拟节点相同,所以进而作为老节点,直接复用之前储存的dom节点,然后比较第一个虚拟节点span的子节点文本2。由于与老节点的子节点中文本节点1不同(文本节点只比较内容),所以会新创建内容为2的文本节点,然后移除之前第一个span的文本节点1。新的虚拟div子节点对比完成后,开始处理没有使用的老虚拟节点,也就是老的第二个span虚拟节点没有被使用,所以会被清空并移除对应的真实dom节点。
由于上面没有加key,虽然最后的渲染结果是正常的,但实际渲染流程并不是我们以为的那样。我们只删除了第一个span虚拟节点,所以只要移除对应的第一个真实span节点就行了。而我们分析后发现先创建了文本节点2,然后移除了文本节点1,再移除了第二个真实span节点。我们知道对于真实的dom节点操作是非常昂贵的,比较耗性能。并且如果是类组件,那就意味着会销毁以前已经实例的组件,然后会新实例一次类组件,再执行一遍这个组件的一些初始生命周期。这样diff效率不高,并且会发生一些意想不到的意外。
如果我们给span设置key,那么我们再分析下渲染对比流程。对应的虚拟节点树为:
//数据为[1,2]的虚拟节点树
{
type: 'div', key: null, props: {
children: [
{
type: 'span', key: 1, props: {
children: 1
}
},
{
type: 'span', key: 2, props: {
children: 2
}
}
]
}
}
//数据为[2]的虚拟节点树
{
type: 'div', key: null, props: {
children: [
{
type: 'span', key: 2, props: {
children: 2
}
}
]
}
}
此时当[1,2]变为[2]后,div的虚拟节点渲染还是同样的逻辑。而在比较第一个span虚拟节点时,首先判断先前第一个span虚拟节点{type:'span',key:1}。虽然type相同但key不同,于是继续从同辈节点中查找,开始与第二个span虚拟节点{type:'span':key:2}对比。发现type与key都相同,于是把这个节点作为新节点对应的老节点。继续比较内容为2的虚拟节点,发现与老节点中子节点2相同,继而复用,这样就不会创建新的文本节点。接下来发现老的虚拟节点{type:'span',key:1}已不在使用,所以移除真实的<span>1</span>的节点。
通过以上分析说明,设置key后,再次渲染组件时逻辑符合我们的预期。所以设置了key能够提升渲染效率,减少一些不必要的意外。
常见问题
1.数组中index可以作为key吗?
如果不会涉及到列表中节点位置变动,比如只是更新了一些项的数据,那么用index作为key与不设置key是一样的,都不会对diff影响什么。只不过不设置key会在react开发模式中控制台有警告信息。其它的情况就不能用index做为key,比如添加或者删除一些项。这会影响节点的位置移动,这时就需要key与项有关联来标识之前的项在何位置,以便增加渲染性能,减少一些意外问题。例如下面的虚拟节点树,当头部新增节点后,如果用index作为key,react在diff过程中会以为更新了前两个节点,并在尾部新增了一个节点。
[
{type:'div',key:0,props:{children:'item 0'}},
{type:'div',key:1,props:{children:'item 1'}}
]
//头部新增项后
[
{type:'div',key:0,props:{children:'item new'}},
{type:'div',key:1,props:{children:'item 0'}},
{type:'div',key:2,props:{children:'item 1'}}
]
2.只有map返回的节点需要设置key吗?
其实不只是能显而易见通过map返回节点的这种结构,只要是同辈相同类型的节点,会发生动态的出现或者不出现,尤其会影响到某个节点重新渲染需要耗费大量时间,也建议设置key来提高渲染性能,以便减少一些不必要的问题。例如下面的组件:
<div>
{this.state.step === 'base' && <div key="base">{this.getBaseForm()}</div>}
{this.state.step === 'fee' && <div key="fee">{this.getFeeForm()}</div>}
</div>