React性能优化 之 不可变数据
什么是不可变数据
不可变数据的概念来自函数式编程。
在函数式编程中,对已初始化的“变量”是不可以更改的,每次更改都要创建一个新的“变量”。
Javascript 在语言层没有实现不可变数据,需要借助第三方库来实现。(immutable.js 或者 immer.js)
为什么使用不可变数据?
使用不可变数据的目的是为了跟踪数据的改变。
如果直接修改数据,那么就很难跟踪到数据的改变,跟踪数据的改变需要可变对象可以与改变之前的版本进行对比,这样整个对象树都需要被遍历一次。
但跟踪不可变数据的变化相对来说就容易多了。
如果发现对象变成了一个新对象,那么我们就可以说对象发生改变了。我们可以很轻松的确定不可变数据是否发生了改变,从而确定何时对组件进行重新渲染。
当一个组件的 props 或 state 变更,React 会将最新返回的元素与之前渲染的元素进行对比,以此决定是否有必要更新真实的 DOM。
当它们不相同时,React 会更新该 DOM。
虽然 React 已经保证未变更的元素不会进行更新,但即使 React 只更新改变了的 DOM 节点,重新渲染仍然花费了一些时间。
在大部分情况下它并不是问题,不过如果它已经慢到让人注意了,你可以通过覆盖生命周期方法 shouldComponentUpdate 来进行提速。该方法会在重新渲染前被触发。
如果你知道在什么情况下你的组件不需要更新,你可以在 shouldComponentUpdate 中返回 false 来跳过整个渲染过程。其包括该组件的 render 调用以及之后的操作。
React性能优化离不开不可变值
shouldComponentUdpate中可以接收两个参数,nextProps和nextState,假如我们通过判断this.props.xxx和nextProps.xxx相等以及this.state.xxx与nextState.xxx相等,可以将返回值设置为false,说明此次并不需要更新子组件:
class CounterButton extends React.Component {
constructor(props) {
super(props);
this.state = {count: 1};
}
shouldComponentUpdate(nextProps, nextState) {
if (this.props.color !== nextProps.color) {
return true;
}
if (this.state.count !== nextState.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>
);
}
}
React v15.3新增加了一个PureComponent类,能够对props和state进行浅比较来减少render函数的执行次数,避免不必要的组件渲染,实现性能上的优化。
什么是浅比较?
答:我们知道JS中的变量类型分为基本类型(number、string、boolean、undefined、null、symbol)和引用类型(function、object、function),基本类型的值保存在栈内存当中,引用类型的值保存在堆内存当中,栈内存中只保存指向堆内存的引用。
而浅比较就是只对栈内存中的数据进行比较。
下面例子使用了PureComponent,而且只改变了数组items里的值,而没改变items的引用地址,所以认为items没有发生变化,不会触发render函数,不会触发组件的渲染。
class App extends PureComponent {
state = {
items: [1, 2, 3]
}
handleClick = () => {
const { items } = this.state;
items.pop();
this.setState({ items });
}
render() {
return (
<div>
<ul>
{this.state.items.map(i => <li key={i}>{i}</li>)}
</ul>
<button onClick={this.handleClick}>delete</button>
</div>
)
}
}
如果想实现组件更新,可以按如下的方式,创建一个新的数组,将新数组的地址赋给items,这样子栈内存中的引用就改变了:
handleClick = () => {
const { items } = this.state;
items.pop();
var newItem = [...items];
this.setState({ item: newItem });
}
immutable.js / immer.js
PureComponent帮我们做了浅层的比较。
如果需要出现引用嵌套引用的数据结构的话,类似下述结构:
handleClick() {
this.setState(state => ({
objA: {
...state.objA,
objB: {
...state.objA.objB,
objC: {
...state.objA.objB.objC,
stringA: 'string',
}
},
},
}));
};
这样子每一层都使用展开运算符,来改变栈内存中的引用地址,太过繁琐。
那为什么不使用深拷贝呢?
答:深拷贝会让所有组件都接收到新的数据,让 shouldComponentUpdate 失效。深比较每次都比较所有值,当数据层次很深且只有一个值变化时,这些比较是对性能的浪费。
视图层的代码,我们希望它更快响应,所以使用 immutable 库进行不可变数据的操作,也算是一种空间换时间的取舍。
immutable.js和immer.js的区别
immutable.js
- 自己维护了一套数据结构,Javascript 的数据类型和 immutable.js 的类型需要相互转换,对数据有侵入性。
- 库的体积比较大(63KB),不太适合包体积紧张的移动端。
- API 极其丰富,学习成本较高。
- 兼容性非常好,支持 IE 较老的版本。
immer.js
- 使用 Proxy 实现,兼容性差。
- 体积很小(12KB),移动端友好。
- API 简洁,使用 Javascript 自己的数据类型,几乎没有理解成本。
学习参考: