React组件的性能优化
高德纳: "我们应该忘记忽略很小的性能优化,可以说97%的情况下,过早的优化是万恶之源,@H_403_8@
而我们应该关心对性能影响最关键的另外3%的代码。"
优化并不嫌早。因为,对性能影响最关键的部分,往往涉及解决方案核心,决定整体的架构,
将来要改变的时候牵扯更大。
1. 单个React组件的性能优化
@H_403_8@但是并不是将之前的渲染内容全部抛弃重来,借助Virtual DOM,React能够计算出对DOM
树的最少修改,这就是React默认情况下渲染都很迅速的秘诀;
2.shouldComponentUpdate的默认实现方式
@H_403_8@从而提升性能,那么我们自然想到使用
shouldComponentUpdate(nextProp,nextState)
shouldComponentUpdate(nextProp,nextState){ return (nextProp.completed !== this.props.completed) || (nextProp.text !== this.props.text) }@H_403_8@
其中nextProps为此次更新传入的props,对于这个组件,影响渲染内容的prop只有completed和text,
只要确保这两个prop没有变化,shouldComponentUpdate就可以返回false阻止没必要的更新
但是,上述的比较只是‘浅层比较’,如果类型是基本类型,只要值相同,那么“浅层比较”
也会认为二者相同:
- 那,如果prop的类型是复杂的对象怎么办?
对象中的内容完全一样也会认为是不同的两个prop。
不光会让代码更加复杂,也可能会造成性能问题。
所以,要想判断前后的对象类型的prop是相同的,就必须要保证prop是指向同一个JavaScript对象:
<Foo styleProp = {{color: "red"}}>@H_403_8@
要避免使用上面的传入方式,应为每次渲染都会重新创建{color: "red"}对象,引用地址每次都不同,将导致每次的styleProp都不同。
const footStyle = {color: "red"};//确保这个初始化只执行一次,不要放在render函数中 <Foo styleProp = {footStyle}>
如果是函数呢?
<Foo onToggle={() => onToggleTodo(item.id)}/>
每次渲染都会产生一个新的函数,这就是问题所在。
-
如果要传递的prop很多呢?
@H_403_8@ - 恩~~用React-Redux的话,有对
shouldComponentUpdate
的默认实现。
3. 对多个React组件的性能优化
@H_403_8@特定的React组件函数,在一个应用中,从上而下有很多React组件组合起来,它们之间的渲染过程要更加复杂。
周期,所以并没有多少优化的事情可做。
所以,也没有什么可优化的空间;
4. React更新阶段的调和(Reconciliation)过程
@H_403_8@使用默认对比,节点过多的话,需要操作的数量太多,而React不可能采用这种算法;
-
节点类型不同的情况
@H_403_8@ - 如果树形节点的类型不相同,那就意味着改动很大,直接认为原来的那个树形结构已经没用,可以扔掉,需要从新构建DOM树,
原有的树形上的React组件便会经历“卸载”的生命周期; - 也就是说,对于Virtual DOM树这是一个“更新”过程,但是却可能引发这个树结构上某些组件的“装载”和“卸载”过程
如:
更新前
<div> <Todos /> </div>
我们想要更新成这样:
<span> <Todos /> </span>
>1. 那么在作比较的时候,一看根节点原来是div,新的是span,类型就不一样了,那么这个算法就废弃之前的div包括里面的所有子节点, 从新构建一个span节点和子节点; >2. 很明显因为根节点不同就将所有的子节点从新构建,这很浪费,但是为了避免O(N*3)的时间复杂度,React这能选择这种比较简单、快捷的方法; >3. 所以,作为开发者,我们一定要避免上面的浪费的情景出现
-
节点类型相同的情况
@H_403_8@ - 如果两个节点类型相同时,对于DOM元素,React会保留节点对应的DOM元素,只对其节点的属性和内容做对比,然后只修改更新的部分;
- 节点类型相同时,对于React组件类型,React做得是根据新节点的props去更新节点的组件实例,引发组件的更新过程;
- 在处理完根节点对比后,React的算法会对根节点的每一个子节点重复一样的操作
-
多个相同子组件的情况
@H_403_8@ - 如果最初组件状态为:
- 更新后为:
- 那么React会创建一个新的TodoItem组件实例,而前两个则进行正常的更新过程但是,如果更新后为:
- (这将暴露一个问题)理想处理方式是,创建一个新的TodoItem组件实例放在第一位,后两个进入自然更新过程
但是要让react按照这种方式,就必须找两个子组件的不同之处,而现有计算两个序列差异的算法时间是O(N*2),显然则
不适合对性能要求很高的场景,所以React选择了一个看起来很傻的办法,即挨个比较每个子组件; - React首先认为把text为First的组件的text改为Zero,Second的改为First,最后创建一个text为Second的组件,这样便会破原有的两个组件完成一个更新过程,并创建一个text为Second的新组件
- 这显然是一个浪费,React也意到,并提供了方克服,不过需要开发人员提供一点帮助,这就是key
- Key的使用
<ul> <TodoItem text = "First" /> <TodoItem text = "Second" /> </ul>@H_403_8@
<ul> <TodoItem text = "First" /> <TodoItem text = "Second" /> <TodoItem text = "Third" /> </ul>@H_403_8@
<ul> <TodoItem text = "Zero" /> <TodoItem text = "First" /> <TodoItem text = "Second" /> </ul>@H_403_8@
key属性可以明确的告诉React每个组件的唯一标识
@H_403_8@<ul> <TodoItem key={1} text = "First" /> <TodoItem key={2} text = "Second" /> </ul>
更新后为:
<ul> <TodoItem key={0} text = "Zero" /> <TodoItem key={1} text = "First" /> <TodoItem key={2} text = "Second" /> </ul>
因为有唯一标识key,React可以根据key值,知道现在的第二和第三个组件就是之前的第一和第二个,便用原来的props启动更新过程,
这样shouldComponentUpdate就会发生作用,避免无谓的更新;
<ul> todos.map((item,index) => { <TodoItem key={index} text={item.text} /> }) </ul>
使用数组下标作为key值,看起来唯一,但不稳定,因为随着todos数组值的不同,同样一个组件实例在不同的更新过程中数组的下标完全可能不同,
把下标当做可以就会让React乱套,记住key不仅要唯一还要确保稳定不可变
需要注意:虽然key是一个prop,但是接受key的组件不能读取key的值,因为key和ref是React保留的两个特殊prop,并没有预期让组将直接访问。