React性能优化之shouldComponentUpdate
前言:
React 构建并维护了一套内部的 UI 渲染描述。它包含了来自你的组件返回的 React 元素。该描述使得 React 避免创建 DOM 节点以及没有必要的节点访问,因为 DOM 操作相对于 JavaScript 对象操作更慢。虽然有时候它被称为"虚拟 DOM",但是它在 React Native 中拥有相同的工作原理。
当一个组件的 props 或 state 变更,React 会将最新返回的元素与之前渲染的元素进行对比,以此决定是否有必要更新真实的 DOM。当它们不相同时,React 会更新该 DOM。
即使 React 只更新改变了的 DOM 节点,重新渲染仍然花费了一些时间。在大部分情况下它并不是问题,不过如果它已经慢到让人注意了,你可以通过覆盖生命周期方法 shouldComponentUpdate
来进行提速,说白了就是shouldComponentUpdate()函数返回true时,才会触发render钩子。该方法会在重新渲染前被触发。其默认实现总是返回 true
,让 React 执行更新:
javascript
shouldComponentUpdate(nextProps, nextState) {
return true;
}
- nextProps: 表示下一个props。
- nextState: 表示下一个state的值。
如果你知道在什么情况下你的组件不需要更新,你可以在 shouldComponentUpdate
中返回 false
来跳过整个渲染过程。其包括该组件的 render
调用以及之后的操作。在大部分情况下,你可以继承 React.PureComponent 以代替手写 shouldComponentUpdate()
。它用当前与之前 props 和 state 的浅比较覆写了 shouldComponentUpdate()
的实现。
shouldComponentUpdate 的作用
这是一个组件的子树。每个节点中,SCU
代表 shouldComponentUpdate
返回的值,而 vDOMEq
代表返回的 React 元素是否相同。最后,圆圈的颜色代表了该组件是否需要被调停。
节点 C2 的 shouldComponentUpdate
返回了 false
,React 因而不会去渲染 C2,也因此 C4 和 C5 的 shouldComponentUpdate
不会被调用到。
对于 C1 和 C3,shouldComponentUpdate
返回了 true
,所以 React 需要继续向下查询子节点。这里 C6 的 shouldComponentUpdate
返回了 true
,同时由于渲染的元素与之前的不同使得 React 更新了该 DOM。
最后一个有趣的例子是 C8。React 需要渲染这个组件,但是由于其返回的 React 元素和之前渲染的相同,所以不需要更新 DOM。
显而易见,你看到 React 只改变了 C6 的 DOM。对于 C8,通过对比了渲染的 React 元素跳过了渲染。而对于 C2 的子节点和 C7,由于 shouldComponentUpdate
使得 render
并没有被调用。因此它们也不需要对比元素了。
举个简单例子:
如果你的组件只有当 props.color
或者 state.count
的值改变才需要更新时,你可以使用 shouldComponentUpdate
来进行检查:
kotlin
class CounterButton extends React.Component {
constructor(props) {
super(props);
this.state = {count: 1};
}
shouldComponentUpdate(nextProps, nextState) {
if (this.props.color !== nextProps.color) { //判断color是否改变
return true;
}
if (this.state.count !== nextState.count) { //判断count是否改变
return true;
}
return false; //如果两个都不改变,则不重新渲染
}
render() {
return (
<button
color={this.props.color}
onClick={() => this.setState(state => ({count: state.count + 1}))}>
Count: {this.state.count}
</button>
);
}
}
在这段代码中,shouldComponentUpdate
仅检查了 props.color
或 state.count
是否改变。如果这些值没有改变,那么这个组件不会更新。如果你的组件更复杂一些,你可似以使用类"浅比较"的模式来检查 props
和 state
中所有的字段,以此来决定是否组件需要更新。React 已经提供了一位好帮手来帮你实现这种常见的模式 - 你只要继承 React.PureComponent
就行了。所以这段代码可以改成以下这种更简洁的形式:
scala
class CounterButton extends React.PureComponent { //继承PureComponent默认进行浅比较,决定
//是否重新渲染
constructor(props) {
super(props);
this.state = {count: 1};
}
render() {
return (
<button
color={this.props.color}
onClick={() => this.setState(state => ({count: state.count + 1}))}>
Count: {this.state.count}
</button>
);
}
}
大部分情况下,你可以使用 React.PureComponent
来代替手写 shouldComponentUpdate
。但它只进行浅比较 ,所以当 props 或者 state 某种程度是可变的话(如数组元素、对象属性),浅比较会有遗漏,那你就不能使用它了。当数据结构很复杂时,情况会变得麻烦。例如,你想要一个 ListOfWords
组件来渲染一组用逗号分开的单词。它有一个叫做 WordAdder
的父组件,该组件允许你点击一个按钮来添加一个单词到列表中。以下代码并不正确:
scala
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'] //words为一个数组,属于引用数据类型
};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
// 这部分代码很糟,而且还有 bug
const words = this.state.words; //words为引入数据类型,变量表示指向地址,与之前指向地址
//相比并没有改变,因此浅比较时认为同一个,不重新渲染
words.push('marklar');
this.setState({words: words});
}
render() {
return (
<div>
<button onClick={this.handleClick} />
<ListOfWords words={this.state.words} />
</div>
);
}
}
问题在于 PureComponent
仅仅会对新老 this.props.words
的值进行简单的对比。由于代码中 WordAdder
的 handleClick
方法改变了同一个 words
数组,使得新老 this.props.words
比较的其实还是同一个数组。即便实际上数组中的单词已经变了,但是比较结果是相同的。可以看到,即便多了新的单词需要被渲染, ListOfWords
却并没有被更新。 当需要对引用数据类型做比较时,还是需要自己手写 shouldComponentUpdate(
不采用继承 PureComponent
的方式),
在内部自己实现比较逻辑,决定是否重新渲染,如根据某个对象的属性去比较:
kotlin
//此时color和count为obj的两个属性,如果采用继承PureComponent的方式,进行浅比较,但一直为同个对象
//这时即使内部属性改变,也检测不出来,导致一直不重新渲染
shouldComponentUpdate(nextProps, nextState) {
if (this.props.obj.color !== nextProps.color) { //判断obj.color是否改变
return true;
}
if (this.state.obj.count !== nextState.count) { //判断obj.count是否改变
return true;
}
return false; //如果两个属性都不改变,则不重新渲染
}
深层次数据变化需要自己手写shouldComponentUpdate
来决定是否重新渲染。