目前工做中用到了React,搭配一块儿使用了Immutable.js。以前没有静下来思考一下为何React社区这么推崇搭配一块儿使用Immutable。正好想写篇文章分析一下这个问题。以前我翻译了React官方文档中Advanced Guides中关于一致化处理(Reconciliation)和性能优化(Optimizing Performance)就涉及到这方面的内容。javascript
一提到React,你们第一时间就想到的虚拟DOM(Virtual DOM)和伴随其带来的高性能。可是React提供的是声明式的API(declarative API),好的一方面是让咱们编写程序更加方便,但另外一方面,却使得咱们不太了解内部细节。java
React采用的是虚拟DOM,每次属性(props)和状态(state)发生变化的时候,render
函数返回不一样的元素树,React会检测当前返回的元素树和上次渲染的元素树以前的差别,而后找出何如高效的更新UI。react
上图展现的就是一个元素树,React比较两次元素树差别的时候,首先从根节点开始。若是元素类型不相同时,该节点如下(包括当前节点)的元素树都会被销毁,树的根节点如下的任何组件都会被卸载,所以状态(state)也会丢失。若是元素节点类型是相同的,那就须要区分是DOM元素仍是组件。若是是DOM元素,会保持节点相同,仅更新改变的属性。而若是是组件的话,会保持组件实例不变,仅更新组件实例的属性,所以组件实例的状态(state)就会被保留下来。比较完当前节点,而后会递归遍历比较子元素。git
一致化处理(Reconciliation)包括的就是React元素的比较以及对应的React元素不一样时对DOM的更新,便可理解为React 内部将虚拟 DOM 同步更新到真实 DOM 的过程,包括新旧虚拟 DOM 的比较及计算最小 DOM 操做。咱们能够看到促使React性能提高的一个重要点就是避免一致化处理。github
shouldComponentUpdate
React使用shouldComponentUpdate
来判别组件是否会由于当前属性(props)和状态(state)变化而致使组件输出变化。默认的shouldComponentUpdate
会在props和state发生变化时返回true
,表示组件会从新渲染,从而调用render
函数。固然了在首次渲染的时候和使用forceUpdate
的时候,是不会通过shouldComponentUpdate
判断。shouldComponentUpdate
做为性能优化的一个很是有用且简单的方法,很是实用。
性能优化
咱们以React官网中的图做为实例:ide
SCU表明shouldComponentUpdate,红色SCU表示shouldComponentUpdate返回true
,绿色的SCU表示shouldComponentUpdate返回false
。vDOMEq表明渲染的React元素是否相等。红色vDOMEq表示React元素不相等,绿色的vDOMEq表示React元素相等。元素节点为红色表示须要对该节点进行一致化处理,节点颜色为绿色表示不须要对其进行一致化处理。函数
表示对于两棵元素树,React会同步比较。在比较C1节点,由于SCU返回的false,须要对其进行diff,vDOMEq返回的是false,故须要一致化处理,存在DOM元素的更新。迭代递归到C2,由于SCU返回的是true,以C2为根节点的整个子树,都不须要diff判断。可是C3的SCU返回true,须要进行diff比较。C3的子节点C6由于SCU返回true须要进行diff比较,而且由于vDOMEq返回的false,所以C6不可避免进行DOM的更新。对于C8来说,经过比较渲染元素而不须要进行一致化处理,而C7由于shouldComponentUpdate返回false从而不须要进行diff。性能
所以咱们能够发现,若是可以合理地编写shouldComponentUpdate
函数,从而能避免没必要要的一致化处理,使得性能能够极大提升。通常shouldComponentUpdate
会比较props
和state
中的属性是否发生改变(浅比较)来断定是否shouldComponentUpdate
是否须要返回true
从而触发一致化处理。咱们能够经过继承React.PureComponent
或者经过引入PureRenderMixin模块来达到目的。可是这也存在一个问题:优化
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() { // This section is bad style and causes a 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> ); } }
组件展示一个以逗号分隔的单词列表,在父组件WordAdder
,当你点击一个按钮时会给列表添加一个单词,但实际上,上面的代码是存在问题的,ListOfWords
继承的React.PureComponent
,当你每次点击按钮会给WordAdder
组件中的this.state.words
添加新的单词。所以ListOfWords
中的shouldComponentUpdate
在判断this.props.words
和nextProps.words
实际是相等的,所以返回了false
。因此,ListOfWords
是不会被从新渲染的,由于React.PureComponent
中的shouldComponentUpdate
进行的是浅比较(shallow comparison),可是若是真的进行深比较,那么比较的性能损耗又太大,不由让咱们得出一个结论:
共享的可变状态是万恶之源
这时候Immutable.js横空出世
Immutable Data是指一旦建立,就不能被更改的数据。对Immutable对象的修改都会返回新的Immutable对象。而且目前的Immutable库,都实现告终构共享,即若是对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享,避免了deepCopy把全部节点都复制一遍带来的性能损耗。比较两个Immutable对象是否相同,只须要使用===
就能够轻松判别。所以若是React传入的数据是Immutable Data,那么React就能高效地比较先后属性的变化,从而决定shouldComponentUpdate
的返回值。解决了上面存在的问题。 可是引入Immutable Data也不是没有代价的,毕竟Immutable Data须要引入新的API,而且须要引入新的库,在原有的项目中引入Immutable Data也是有风险和代价的并且还须要开发者转变原有的思惟(毕竟天下没有白吃的午饭)。