本文正在参加「金石计划 . 瓜分6万现金大奖」
虚拟DOM 很好,但别贪杯。
React对页面更新都是对组件的重新渲染,将之前渲染的内容全部再渲染一次。而是利用虚拟DOM来达到最小化修改真实DOM,达到高效的更新。
但是,有时明确知道渲染结果不会有变化,那么这时对虚拟DOM的计算就是“一次浪费”。
所以如果能够在开始计算虚拟DOM之前就可以判断,那样可以干脆不要进行虚拟DOM计算和比较,速度就会更快。
注意,这里说的浪费是计算Virtual DOM的浪费,并不是访问DOM树的浪费。
单个 React 组件的性能优化
shouldComponentUpdate
作为一个生命周期函数,shouldComponentUpdate
函数则决定“什么时候组件不需要重新渲染”。
这个函数默认返回是true,也就是默认每次更新的时候都要调用所有的生命周期函数。包括调用render函数,根据render函数的返回结果计算虚拟DOM。
当返回false时,就不会进行组件的更新。我们需要根据自身考虑自定义改写返回false的情况。
<App style={{color:'red'}} onClick={() => sayHello()} />
shouldComponentUpdate
函数实现,每一次渲染都会认为style这个prop发生了变化,因为每次都会产生一个新的对象给style,在“浅层比较”中,类似==
先判断对象的指针,这里创建了新对象{color:'red'}
,即使里面内容一样、也会认为是传入了一个新对象,即新props。那么App组件会重新渲染。
这里赋值给onClick的是一个匿名的函数,而且是在赋值的时候产生的。也就是说,每次渲染App组件的时候,都会产生一个新的函数,也会造成App组件会重新渲染。
如果需要做“深层比较”,那就是某个特定组件的行为,需要开发者自己根据组件情况去编写。不过也要谨记,不要简单地递归比较所有层次的字段,因为传递进来的prop对象什么结构是无法预料的。
这里官网有具体实现方式
React.PureComponent 、 React.memo做“浅层比较”
大部分情况下,可以使用 React.PureComponent
来代替手写 shouldComponentUpdate
但它只进行浅比较,所以当 props 或者 state 某种程度是可变的话,浅比较会有遗漏,那你就不能使用它了。
例如当state
是一个对象时,传入给子组件,即便state
内部属性值改变。子组件也依然会认为props并未改变而不会更新。这也就是为什么React推荐使用不可变数据
。
这里引用官网的例子
class ListOfWords extends React.PureComponent {
render() {
return <div>{this.props.words.join(',')}</div>;
}
}
class WordAdder extends React.Component {
constructor(props) {
super(props);
this.state = {
words: ['marklar']
};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
// 这部分代码很糟,而且还有 bug
const words = this.state.words;
words.push('marklar');
this.setState({words: words});
}
render() {
return (
<div>
<button onClick={this.handleClick} />
<ListOfWords words={this.state.words} />
</div>
);
}
}
解决方法有很多,但是核心一点就是,不要直接修改原来的state而是要复制一个新的state在新的state的基础上进行改进。
多个React组件的性能优化
多个React组件之间组合的渲染过程。和单个React组件的生命周期一样,React组件也要考虑三个阶段:装载阶段、更新阶段和卸载阶段。
装载阶段和卸载阶段都是组件必须经历一次的,这无可避免,也没有什么太多可以考虑的点。 那么我们主要看更新阶段。
React的调和(Reconciliation)过程
在装载过程中,React通过render方法在内存中产生了一个树形的结构,树上每一个节点代表一个React组件或者原生的DOM元素,这个树形结构就是所谓的Virtual DOM。React根据这个Virtual DOM来渲染产生浏览器中的DOM树。
Reconciliation(调和)就是React找寻新旧虚拟DOM的不同的过程。
按照计算机科学目前的算法研究结果,对比两个N个节点的树形结构的算法,时间复杂度是O(N3),但是为了降低算法时间复杂度,React做出了部分割舍。
1.节点类型不同的情况
如果树形结构根节点类型不相同,重新构建新的DOM树,原有的树形上的React组件会经历“卸载”的生命周期,即便可能这颗树的某些节点可以复用,为了避免O(N3)的时间复杂度,React必须要选择一个更简单更快捷的算法,也就只能采用这种方式。
这里旧组件会经历卸载过程,新组件会经历装载过程。
2.节点类型相同的情况
如果两个树形结构的根节点类型相同,React就认为原来的根节点只需要更新过程,不会将其卸载,也不会引发根节点的重新装载。
根据组件类型进行不同处理
- DOM元素组件
对于DOM元素类型,React会保留节点对应的DOM元素,只对树形结构根节点上的属性和内容做一下比对,然后只更新修改的部分。
- React组件
React能做的只是根据新节点的props去更新原来根节点的组件实例,引发这个组件实例的更新过程。
在处理完根节点的对比之后,React的算法会对根节点的每个子节点重复一样的动作,这时候每个子节点就成为它所覆盖部分的根节点,处理方式和它的父节点完全一样。
3.多个子组件的情况
找出两个子组件序列的不同之处,现有的计算出两个序列差异的算法时间是O(N2)。同样为了降低时间复杂度,React选择了不是寻找两个序列的精确差别,而是直接挨个比较每个子组件。
当新添加子组件不是在末尾插入时,子组件之间的顺序被打乱了,就会造成旧的子组件不能被复用的处境(只能触发旧组件的更新)。
这里React又推出了一个Key关键属性来解决这个问题
key
key属性就像身份证一样,每个组件都可以拥有一个独一无二的key属性用于标识。
当新添加子组件不是在末尾插入时,子组件之间的顺序被打乱了,但是我们有key属性,所以我们就可以靠key属性来尽可能复用旧子组件(避免旧组件的更新)。
注意:
key值虽然能够在每个时刻都唯一,但是变来变去,那么就会误导React做出错误判断,甚至导致错误的渲染结果。也就是要保证key的值在组件的装载->更新->卸载的过程都得保证一致。
const todoItems = todos.map((todo, index) =>
// Only do this if items have no stable IDs <li key={index}> {todo.text}
</li>
);
如果列表项目的顺序可能会变化,我们不建议使用索引来用作 key 值,因为这样做会导致性能变差,还可能引起组件状态的问题。可以看看 Robin Pokorny 的深度解析使用索引作为 key 的负面影响这一篇文章。翻译版
如果你选择不指定显式的 key 值,那么 React 将默认使用索引用作为列表项目的 key 值。
参考
《深入浅出React和Redux》 《React官网》