前段时间ReactJS发布的 v15.3.0 中针对ES6语法,增加了一个新的组件基类:React.PureComponent。
之前,使用ES6语法开发的同学,为了避免不必要的render开销,可能会像官方文档介绍的那样,这样来使用PureRenderMixn:
1 2 3 4 5 6 7 8 9 10 11 12
| import PureRenderMixin from 'react-addons-pure-render-mixin'; class FooComponent extends React.Component { constructor(props) { super(props); this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this); }
render() { return <div className={this.props.className}>foo</div>; } }
|
或者直接这样:
1 2 3 4 5 6 7 8 9 10
| var shallowCompare = require('react-addons-shallow-compare'); export class SampleComponent extends React.Component { shouldComponentUpdate(nextProps, nextState) { return shallowCompare(this, nextProps, nextState); }
render() { return <div className={this.props.className}>foo</div>; } }
|
有了React.PureComponent之后,可以这样:
1 2 3 4 5
| export class SampleComponent extends React.PureComponent { render() { return <div className={this.props.className}>foo</div>; } }
|
实际上跟之前两种方式是等价的,但是写起来会更加简介优雅。
好,背景介绍完了,下面进入正题,聊一聊 React.PureComponent
、PureRenderMixin
、shallowCompare
如何帮助我们避免额外的 render 开销,提高性能,以及为什么说他们配上 ImmutableJS才更有意义。
按照我们直观的理解,当我们使用了 PureComponent
作为组件基类时,如果组件的props或者state没有发生变化,就不应该重新渲染组件,这里说的 “没有发生变化”,不是指语言层面的 === 或者 ==,而是指新的 props 或者 state 不会对组件的渲染结果产生任何的影响。
看下面这个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
| class Sample extends React.PureComponent{ constructor(props) { super(props); this.state = { name: 'Lucy', pet: { type: 'cat', color: 'red', } }; } componentDidUpdate() { console.log('did update'); } change() { this.setState({ name: this.refs.name.value, pet: { color: this.refs.petColor.value, type: this.refs.petType.value } }); } render() { const name = this.state.name; const petC = this.state.pet.color; const petT = this.state.pet.type; return ( <div> <strong></strong> <div>{name}'s pet is a {petC} {petT}.</div> <hr/> <p> name: <input type="text" ref="name" defaultValue={name}/> </p> <p> pet color: <input type="text" ref="petColor" defaultValue={petC}/> </p> <p> pet type: <input type="text" ref="petType" defaultValue={petT}/> </p> <button onClick={() => this.change()}>Change</button> </div> ); } }
ReactDOM.render(<Sample/>, document.getElementById('root'));
|
在这个例子中,直接点击 “Change” 按钮,不会对 state 产生任何影响组件最终渲染结果的更改,在这种情况下我们是期望组件不要重新渲染的。
我在 componentDidUpdate 中添加了一条日志输出,如果控制台有输出 “did update” 则说明组件被重新渲染了。
现在直接点击 “Change” 按钮,能看到控制台输出“did update”,这是为什么呢?
NOTE: 即便是使用 PureRenderMixn和shallowCompare都是一样的,不信可以自己试验一下,文章后面的内容也会说明这一点。
为了弄清楚这个问题,让我们扒拉一下React的源码。
代码连接
1 2 3 4 5
| if (this._compositeType === CompositeTypes.PureClass) { shouldUpdate = !shallowEqual(prevProps, nextProps) || !shallowEqual(inst.state, nextState); }
|
这里的 shouldUpdate 变量就是在后面的逻辑中标识该不该重新渲染组件的,这里有个 shallowEqual
函数,我们暂且不表。
我们再看一下 PureRenderMixin 的代码:
代码链接
1 2 3 4 5 6 7
| var ReactComponentWithPureRenderMixin = { shouldComponentUpdate: function(nextProps, nextState) { return shallowCompare(this, nextProps, nextState); }, };
module.exports = ReactComponentWithPureRenderMixin;
|
这个Mixin就是帮我们实现了 shouldComponentUpdate
函数,原来这里用到了 shallowCompare
方法,那好,我们继续看看 shallowCompare
方法的代码:
代码链接
1 2 3 4 5 6
| function shallowCompare(instance, nextProps, nextState) { return ( !shallowEqual(instance.props, nextProps) || !shallowEqual(instance.state, nextState) ); }
|
OK!OK! 看到了吧,shallowCompare
也是对 shallowEqual
的封装,所以文章开头描述的三种方式归根揭底都是一样的。
那么我们现在只要搞清楚 shallowEqual
方法是怎么实现的,上面的问题就真相大白了,看代码:
代码链接
NOTE: shallowEqual
不是ReactJS的代码,它是Facebook的一个工具库:fbjs。
这个方法重点关注两个点:
- 如它的名字一样,这个方法只进行对象的浅比较,我们知道deepCompare是无脑递归操作,开销会比较大,得不偿失的。
- 比较对象属性的值,用的是 Object.is 方法。
1 2 3 4 5 6 7 8 9 10 11 12
| Object.is( { pet: { color: "red", type: "cat" } }, { pet: { color: "red", type: "cat" } } ) === false;
|
所以说最终原因是因为我们的 state 嵌套了一个 pet 对象,更新 state 时,pet被换成了一个新的对象,导致浅比较通过不了。
问题原因找到了,由此我们可以看到,shallowCompare
只会当组件的 state 或者 props 没有嵌套结构的时候才会正确按照预期发挥作用,然而在实际的项目中,嵌套的 state 或者 props 结构是很常见的,所以我认为单纯使用React.PureComponent
是实际应用中时没有什么卵用的,那我们如何规避这个问题以满足我们的期望呢,这就是 ImmutableJS 的意义。
ImmutableJS 是为了解决 JavaScript 语言层面上没有不可变 Data 的问题,ImmutableJS 提供了许多不可变的数据结构,对原始数据的更新会生成一个新的 Immutable 对象,所以可以放心大胆的操作;同样,如果一个操作没有对数据的值进行实质性的更新,那么操作的结果还是跟操作之前的一模一样,这也是通过 ImmutableJS 可以解决我们今天的问题的关键所在。另外 ImmutableJS 还提供了功能强大且方便的API,想要了解更多信息可以查看其 官方文档,这里就不展开赘述了,熟悉 ImmutableJS 的API之后,你就会发现它能做的时候远不止本文中这一丢丢。
下面我们使用ImmutableJS对之前的例子进行改造,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| class Sample extends React.PureComponent{ constructor(props) { super(props); this.state = { name: 'Lucy', pet: Immutable.fromJS({ type: 'cat', color: 'red', }) }; } ... change() { this.setState({ name: this.refs.name.value, pet: this.state.pet .set('color', this.refs.petColor.value) .set('type', this.refs.petType.value) }); } render() { const name = this.state.name; const petC = this.state.pet.get('color'); const petT = this.state.pet.get('type'); ... }
...
|
现在我们再点击 “Change”按钮,控制台就不会再输出“did update”了。