react为什么需要key

多个react组件性能优化

1.1 生命周期

  1. 当一个react组件被装载、更新和卸载时,组件的一系列生命周期函数会被调用。不过这些生命周期函数是针对某一个特定的react组件的函数,在一个应用中,从上到下有很多react组件组合起来,它们之间的渲染过程更加复杂。

  2. 其中,装载阶段基本上没有什么选择,当一个react组件第一次出现在DOM树中时,无论如何都是要彻底渲染一次的,从这个react组件往下的所有子组件,都要经历一遍react组件的装载生命周期,因为这部分的的工作没有什么可以省略的,所以没有多少性能优化的事情可以做。

  3. 至于卸载阶段,只有一个生命周期函数 componentWillUnmount,这个函数做的事情知识清理 componentDidMount 添加的事件处理监听等收尾工作,做的事情比装载过程要少很多,所以也没什么可以优化的空间。

  4. 所以值得关注的过程,就只剩下了更新过程。

1.2 react的调和(reconciliation)过程

react在更新阶段很巧妙地对比原有的 Virtual DOM 和新生成的 Virtual DOM ,找出两者的不同之处,根据不同来修改DOM树,这样只需要做做小的必要改动。react在更新这个“找不同”的过程,就叫做(reconciliation)过程。

1.3 react的算法时间复杂度

  1. Facebook推出的react之初打出的旗号之一就是“高性能”,所以react的(reconciliation)过程必须快速。但是,找出两个树形结构的区别,从计算机科学的角度来说,真的不是一件快速的事。
  2. 按照计算机科学目前的算法研究结果,对比两个N个节点的树形结构的算法,时间复杂度是 O(N^3),打个比方,假如两个树形结构上各有100节点,那么找出这两个树形结构差别的操作,需要100*100*100次操作,也就是一百万次当量的操作,假如有一千个节点,那么需要相当于1000*1000*1000次操作,这是一亿次的操作当量,这么巨大数量的操作在强调快速反应的网页中是不可想象的,所以react不可能采用这样的算法。
  3. react实际采用的算法需要的时间复杂度是 O(N) ,因为对比两个树形怎么着都要对比两个树形上的节点,似乎也不可能有比 O(N) 时间复杂度更低的算法。react采用的算法肯定不是最精准的,但是对于react英语的场景来说,绝对是性能和复杂度的最好折衷,让这个算法发挥作用,还需要开发者一点配合。
  4. 其实,react的reconciliation算法并不是很复杂,当react要对比两个 Virtual DOM 的树形结构的时候,从根节点开始递归往下比对,在树形结构上,每个阶段都可以看做一个这个阶段以下部分子树的根节点。所以其实这个对比算法可以从 Virtual DOM 上任何一个节点开始执行。

1.4 react检查两个树形结构的根节点的不同处理方式

  1. 节点类型不同的情况

如果树形结构的根节点类型不相同,那就意味着改动太大了,也不需要去费心考虑是不是原来那个树形的根节点被移动到其他地方去了,直接认为原来那个树形结构会已经没用,可以扔掉,需要重新构建新的DOM树,原有的树形结构上的react组件都会经历“卸载”的生命周期。也就是,对于 Virtual DOM 树这是一个“更新”的过程,但是却可能引发这个树形结构上某些组件的“装载”和“卸载”过程。例如,在更新之前,组件的结构是这样:

1
2
3
<div>
<Todos />
</div>

我们想要更新成这样:

1
2
3
<span>
<Todos />
</span>

那么react会认为要废掉之前的div节点,包括下面的所有的子节点,一切推到重新来,重新构建一个span节点以及其子节点。看似浪费,但是为了避免 O(N^3) 的时间复杂度,react必须要选一个更简单更快的算法,也就只能用这种方式。

作为开发者,很显然要避免作为包裹功能的节点类型被随意改变。

  1. 节点类型相同的情况

如果两个树形结构的根节点类型相同,react就会认为原来的根节点只需要更新过程,不会将其卸载,也不会引发根节点的重新装载。这时候要区分一下节点类型。节点可以分为两类:一类是DOM元素类型对应的就是HTML直接支持的元素类型,比如div,span,p。另一类是react组件,也就是利用react库订制的类型。

对于DOM类型,react会保留节点对应的DOM元素,支队树形结构根节点上的树形和内容做一下对比,然后只更新修改的部分。
比如原来的jsx表示是这样:

1
2
3
<div style={{color:"red",fontSize:16}} className="welcome">
hello
</div>

改变后jsx表示是这样的:

1
2
3
<div style={{color:"green",fontSize:16}} className="farewell">
good bye
</div>

这两个的差别就是颜色和className发生了改变,在操作DOM树节点的时候,只修改这些发生变化的部分,让DOM操作尽可能少。

如果树形结构的根节点不是DOM元素类型,react所做的工作类似,只是react不知道如何去更新DOM树,因为这些逻辑还在react组件之中,react能做的只是根据新节点的props去更新原来根节点的组件实例,引发这个组件的实例的更新过程。也就会触发react更新阶段的生命周期函数,这个过程中,如果 shouldComponentUpdate 返回false。更新就会停止。

  1. 多个子组件的情况

当一个组件包含多个子组件的情况,react的处理方式也非常简单的直接。例如最初组件形态用jsx表示是这样:

1
2
3
4
<ul>
<TodoItem text="first" completed={false} />
<TodoItem text="second" completed={false} />
</ul>

更新之后,jsx表示是这样的:

1
2
3
4
5
<ul>
<TodoItem text="first" completed={false} />
<TodoItem text="second" completed={false} />
<TodoItem text="third" completed={false} />
</ul>

这种情况react的更新过程,只要 shouldComponentUpdate 使用恰当,可以避免实质性的更新操作。

但是,在序列前面增加一个 TodoItem实例的时候。需要更新后的形态jsx表示为:

1
2
3
4
5
<ul>
<TodoItem text="zero" completed={false} />
<TodoItem text="first" completed={false} />
<TodoItem text="second" completed={false} />
</ul>

这种情况下,react会首先认为把text为first的组件实例的text改为zero,text为second的组件实例的text改为first,最后增加一个一个实例,text为second。这种情况下,shouldComponentUpdate 也无法判断是否返回false。想要增加一个组件实例,却引发了另外两个组件的更新。

这种情况,react的key就凸显出作用了。react通过这个key就能判断每一个组件在组件序列中的位置了。

上面的情况变为,最初形态为:

1
2
3
4
<ul>
<TodoItem key={1} text="first" completed={false} />
<TodoItem key={2} text="second" completed={false} />
</ul>

新增组件的时候,形态为:

1
2
3
4
5
<ul>
<TodoItem key={0} text="zero" completed={false} />
<TodoItem key={1} text="first" completed={false} />
<TodoItem key={2} text="second" completed={false} />
</ul>

react就知道把组件插在组件序列的第一位了。对原有的两个组件,只用props来启用更新过程,_shouldComponentUpdate__ 就能根据key来判断是否返回false来中断更新了,避免无用的更新操作了。

1.5 总结

所以,碰到数组->列表的映射,或是同级元素需要移位的情况,一定要给元素加上key属性!