React中的key

805 阅读4分钟

前言

React中的一个最佳实践是:在渲染一系列相同类型的兄弟元素时,给每个元素指定一个稳定、可预测、兄弟间唯一的key值,这样做可以避免某些场景下的错误渲染并且提升React的渲染性能。

React的渲染策略

​ 为什么指定key值之后可以带来上述的收益?一切要先从React的渲染策略说起。

​ 我们知道React的工作机制是维护一棵虚拟DOM树(JS对象),其结构与浏览器中的DOM树保持某种映射关系。每次组件更新(props、state、context等发生变化时)会先生成一棵新的虚拟DOM树,然后通过其Diff算法比较两棵树之间的差异,最后一次性地将这些差异“打入”浏览器的DOM中,从而实现了浏览器界面的更新。

​ 理论上要比较两棵树差异,算法的时间复杂度会达到O(n3),在React中Diff算法为了降低时间复杂度而设定了两个规则:

  1. 类型不同则元素不同
  2. 类型相同则根据key值(如果有)是否相同来判断元素是否相同

根据自己的经验我再补充两个规则便于理解

  1. 类型相同且没有key的情况下,默认以渲染顺序作为key值
  2. 渲染中出现多个相同层级相同类型且相同key的元素时,只渲染其中的第一个元素

设定了规则之后React的Diff算法时间复杂度降到了O(n)

​ 上述的规则可以帮助React更便捷地判断两个元素是相同的还是不同的,遇到相同的元素则更新其props(如果是组件类型还会触发相应的生命周期),遇到不同的元素则整个替换掉(或销毁、或新增)

key是如何帮助提高渲染性能的?

​ 假设某次React更新产生的两棵虚拟树如下

//old tree
<ul>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

//new tree
<ul>
  <li>Connecticut</li>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

从我们人类的角度一眼就能看出后者与前者的差异只是在开头多了一个li元素,所以如果React足够聪明,它只要在浏览器DOM里添加一个新的元素就可以了。但在React看来,根据前面所说的规则,它会默认把Duke和Connecticut看作同一元素,而Villanova和Duke看作同一元素,而新树的Villanova则是一个全新的元素,这样一来React实际会先更新浏览器DOM中的前两个元素,再插入第三个新元素,比我们认为的最佳做法多出了两次更新操作。

​ 而如果加了key呢?假设某次React更新产生的两棵虚拟树如下

//old tree
<ul>
  <li key="Duke">Duke</li>
  <li key="Villanova">Villanova</li>
</ul>

//new tree
<ul>
  <li key="Connecticut">Connecticut</li>
  <li key="Duke">Duke</li>
  <li key="Villanova">Villanova</li>
</ul>

这种情况下React会根据key值判断出Connecticut是新元素,而另两个li元素则没有变化无需更新,所以React只会在浏览器DOM中插入新元素。相比没有指定key的情况,要提升了渲染效率。

反模式:用index作为key

​ 虽然为兄弟元素指定key值可以提升性能,但需要强调的是key值最好要保持稳定、可预测、兄弟间唯一。一种常见的反模式是用index作为key,比如下面的例子:

class Home extends React.Component {
	constructor() {
		super();
		this.state = {
			list: [
        {name:'zs',id:1},
	      {name:'ls',id:2},
        {name:'ww',id:3},
      ],
		};
	}
	addAheadItem() {
		this.setState({
			list: [{name:'zq',id:4}, ...this.state.list]
		});
	}
render() {
		return (
			<div>
				<button	onClick={() => {this.addAheadItem();}}>
					添加到头部
				</button>
				//使用index作为索引
				<div>
					{this.state.list.map((item, index) => {
						return (
							<li key={index}>
								{item.name}
								<input type="text"  />
							</li>
						);
					})}
				</div>
				//使用id作为索引
				<div>
					{this.state.list.map((item, index) => {
						return (
							<li key={item.id}>
								{item.name}
								<input type="text"  />
							</li>
						);
					})}
				</div>
			</div>
		);
	}
}
render(<Home/>,document.getElementById('root'));

这个例子里面假如我们先在input框里输入任意内容,再点击按钮,会发现以index作为key的部分,input内容“错位”了,产生这种现象的原因是:由于React根据index去区分元素,所以虽然我们认为我们在头部新增了一个元素,但在React看来却是在末尾增加了一个元素,而且前面两个元素更新了属性,再加上这里的input是非受控组件,所以它没有任何变化,结果就是第一行的标题从"zs"变成了"zq",而input内容没有变。

​ 从这个反模式的例子可以看出,使用index作为key不但容易增加渲染开销,而且可能会产生bug,更好的方式可以是利用npm包shortid, 它可以快速的生成一系列‘短的 无序的 对url友好的 唯一的’ id,用这些id作为key来标识兄弟元素可以避免反模式的问题。