真理也是一个幻觉,不过,是一个我们的生存,无法须弥或缺的幻觉。——《当尼采哭泣》
1.性能问题的引出
对于上篇文章里面所出现的react应用来说,我给todos组件的reducer函数所接受的state参数设置默认值为一个具有10000个todo项的数组,由此渲染的时候,当我们想要对某一个todo进行反转操作以及删除操作的时候发现此时的性能极差,如上动图所示,当只反转一个todo的时候,很显然此时差不多有了五秒的计算时间,,,。
2.为什么会出现性能问题
虽然react已经提供了较好的渲染性能,但是还是存在可以优化性能的地方。每一次页面的更新都是对组件的重新渲染,但是并不是将所有之前渲染的组件都全部抛弃重来,有可能只是更新,差一点的话那就是卸载接着更新,最好的情况就是没有发生变化的组件不需要进行更新过程。首先我们需要知道的是,当页面由局部响应引起更新时,virtual dom能够在自己的理解范围内安全无误的计算出对dom树所需要做出的最少修改。
react组件的生命周期可以分为三个阶段:挂载,更新,卸载。
挂载过程的生命周期函数有:
- componentWillMount
- render
- componentDidMount
更新过程,父组件向下传递props或组件自身执行setState方法时触发更新,生命周期函数有:
- shouldComponentUpdate
- componentWillUpdate
- render
- componentDidUpdate
卸载过程的生命周期函数有:
- componentWillUnmount
shouldComponentUpdate以及render函数中最为重要的方法。render方法决定了组件应该渲染出什么,而对于shouldComponentUpdate方法来说,它则决定了组件什么时候可以不必被渲染。对于Component类所提供的shouldComponentUpdate来说,它的默认实现是始终返回true的,因此在发生更新过程的时候,组件是会默认调用大多数生命周期函数,包括render函数,而render函数的结果就是virtual dom。正是由于这个原因,导致有的时候react网页的响应时的性能可能表现的不那么理想,如果想要优化性能的话,那么就尝试从shouldComponentUpdate方法下手。
我们需要注意的是,对于利用无状态函数所实现的组件来说,它的shouldComponentUpdate方法默认是继承Component类的该方法的,即保持更新。。而对于一个无状态函数实现的组件来说,它是修改不了shouldComponentUpdate方法的。。当然,对于这种组件的优化方法我们在后面会另作介绍。
当我们使用shouldComponentUpdate方法时,一般的做法是利用===比较符将nextProps和this.props以及nectState和this.state作比较。但是这种比较符是浅层的,对于两个拥有者两个相同值的不同对象来说,===是不等的。但是虽然此时两个对象是不等的,可当它作为同一个组件的props来说,这个组件在view层却并不用体现什么变化。幸亏对于那些基本数据类型变量来说,===比较符不会出现这种情况。
var obj1={"a":1};
var obj2={"a":2};
obj2 === obj1;//false
3.怎么优化性能
在上面我们也提到了想要优化组件性能的话,那就是对每个组件都实现其各自的shouldComponentUpdate方法。此时有两个问题:
- 1.对于react-redux应用来说,我们一般将组件分为容器组件以及视图组件,对于容器组件来说,react-redux替我们封装了逻辑她完全由connect方法主导产生,那么我们又该如何定义容器组件的shouldComponentUpdate呢?
- 2.可是对于无状态函数实现的组件来说,又得怎么为其定义呢?
- 3.在上面也同时提到了,当我们实现shouldComponentUpdate的时候,常规做法就是利用===比较前后props以及前后state。可是当具有相同值的不同对象作为一个组件的props的话,那么这个组件单单利用===实现shouldComponentUpdate的话,那么这个组件将会进行重渲染。那么又该如何避免这种情况呢?
//利用===逻辑实现shouldComponentUpdate将无济于事
<Song data={"url": "http://music.163.com/#/m/song?id=28068836&userid=67923532"} />
那么对于上面提到的三个问题又有什么解决办法呢?
1.对于由react-redux提供的connect方法来说,由它实现的容器组件会有一个与父类不同的shouldComponentUpdate实现,问题是对于容器组件的shouldComponentUpdate函数来说判断了哪些内容。我们知道对于容器组件来说,他自己可能接受props,容器组件不负责show,容器组件对应的子视图组件才负责被用户响应以及show。那么问题来了,这个容器组件的shouldComponentUpdate比较了那些内容?答案是即比较了自身的props也比较了自身的state——这些state会被mapStateToProps函数处理为作为视图组件的props。因此react-redux替容器组件实现的shouldComponentUpdate实现了自身在什么时候得重新渲染。需要如果我们的视图组件如果是无状态函数组件的话,那么connect之后还是无状态函数组件。那么问题又来了,我们的视图组件的shouldComponentUpdate又如何实现?如果父组件阻断了重渲染的话,那么它的子组件还会进行重渲染吗?已知这个子组件和virtual dom树的唯一联系就是通过其父组件,目前为止,我的猜测是既然他不会被无故牵连那么按理说不会平白无故被进行重渲染。????
2.对于无状态函数组件来说,如果我们希望给定义一个shouldComponentUpdate函数的话,那么按理来说是办不到的。毕竟这个组件都不是以class形式定义的,那么问题来了,该如何解决?答案是通过react-redux的connect,connect?对,我们可以利用connect方法为无状态函数组件包装一下,由于它不同于容器组件的子组件——视图组件,所以在connect的时候,不需要传入mapStateToProps以及mapDispatchToProps这两个参数。像下面这样的形式:
export default connect()(无状态函数组件)
需要注意的是,此时我们的无状态函数组件依旧是一个无状态组件,因此它是没有自己定义的shouldComponentUpdate?
3.在这种情况下,我们应该给组件传入一个只被创建一次的变量。当然,事实上,这种问题远比上面提到的那各个更加难以解决,但是要注意的就是别犯下面的错误:
<Funk style={{"color": "#233"}} />
<Rock song={() => {console.log("NARUTO")}} />
4.todoApp的具体优化
综上所诉,我们可以在我们的todoApp里寻找优化路径,比如像下面这样所列举出来的:
//todos/view/addTodo.js line 55
style = {{"width": "600px"}}
这样的话,对于react的shouldComponentUpdate来说,利用浅层比较符的话它识别不了style prop 并没有对造成渲染view产生什么不同的影响。因此会重复渲染,改进方式也很简单:
const inputStyle = {"width": "600px"};
style={inputStyle}
像上面这样的形式在我们的视图组件里面还存在很多,像下面这样的:
//todos/view/listItem.js
<li style={{.....}} />
<button style={{.....}} />
<span style={{.....}} />
上面所列举出来的性能问题和上面的情况是一样的,所以优化方式也没有什么差别,问题是对于span的style props来说,它的某个style的属性是必须依赖组件的finished props的,因此,暂时想不出方法对他进行进一步优化。
还有就是对于一个问题就是对于无状态组件的优化问题,上面也提到了那就是利用react-redux提供的connect方法:
export default connect()(ListItem)
对于todoList文件,存在着一个较难优化的点:
//line 33, line 34
handleReverse={() => {handleReverse(todo.id)}}
handleDelete={() => {handleDelete(todo.id)}}
在这里我们给ListItem传入了handleReverse以及handleDelete这两个props,但是这两个props的值在每次创建ListItem组件的时候都会创建一次,所以我们需要给他传入一个只创建一次的函数,但是也得顺利完成这里的逻辑,那么可以像下面这样做:
<ListItem
key={todo.id}
id={todo.id}
value={todo.text}
handleReverse={handleReverse}
handleDelete={handleDelete}
finished={todo.finished}
/>
相应的在listItem文件增加下面这些逻辑
const mapStateToProps = (state, ownProps) => ({
"value": ownProps.value,
"finished": ownProps.finished
})
const mapDispatchToProps = (dispatch, ownProps) => ({
"onReverse": () => {ownProps.handleReverse(ownProps.id)},
"onDelete": () => {ownProps.handleDelete(ownProps.id)}
})
export default connect(mapStateToProps, mapDispatchToProps)(ListItem)
此时,最基本的性能问题已经差不多解决,下面可以看看效果: