React爬坑秘籍(一)——提高渲染性能

React爬坑秘籍(一)——提高渲染性能

##前言

来到腾讯实习后,有幸八月份开始了腾讯办公助手PC端的开发。由于办公助手主推的是移动端,因此导师也是大胆的让咱们实习生来技术选型并开发,他来作code review。以前也学习过React,固然也是很是合适这一次的开发。javascript

我会梳理这一个月来,本身对架构的思考过程和踩过的坑。固然这一切都不必定是最佳的,因此但愿能有更多的建议和讨论。前端

例子所需库:Webpack、React、Immutable。其中Webpack用于前端构建,若是不清楚的同窗能够看这里:webpack前端构建体验java

##出现场景

通常来讲,React做为一个高效的UI Library,若是合理使用是很难出现性能问题的。它内部提供了虚拟DOM搭配上Diff算法,和子组件必要的key属性,都是很是优秀的优化了绝大部分的性能。react

可是咱们来模拟一个场景,在一个数组里有10000个对象,咱们把这个数组的数据渲染出来后,其中一个属性用于控制页面状态。webpack

在这里我但愿你们知道有一点就是,当父组件的状态state发生变化时,传入state的子组件都会进行从新渲染。git

下面咱们来模拟一下这种状况,一块儿来看看。github

/** * Created by YikaJ on 15/9/17. */ 'use strict'; var React = require("react"); var App = React.createClass({ getInitialState(){ return { list: this.props.dataArr } }, // 对数据的状态进行变动 toggleChecked(event){ let checked = event.target.checked; let index = event.target.getAttribute("data-index"); let list = this.state.list; list[index].checked = checked; this.setState({list}); }, render(){ // 将数组的数据渲染出来 return ( <ul> {this.state.list.map((data, index)=>{ return ( <ListItem data={data} index={index} key={data.name} toggleChecked={this.toggleChecked} /> ) })} </ul> ) } }); // 表明每个子组件 var ListItem = React.createClass({ render(){ let data = this.props.data; let index = this.props.index; // checkbox选择框是一个受限组件,用数据来决定它是否选中 return ( <li> <input type="checkbox" data-index={index} checked={data.checked} onChange={this.props.toggleChecked}/> <span>{data.name}</span> </li> ) } }); // 构造一个2000个数据的数组 let dataArr = []; for(let i = 0; i < 2000; i++){ let checked = Math.random() < 0.5; dataArr.push({ name: i, checked }); } React.render(<App dataArr={dataArr}/>, document.body);

这个就是咱们的有性能问题的组件。当咱们去点击选框时,由于父组件的state传到了子组件的props里,咱们就会遇到10000个子组件从新渲染的状况。因此表现出来的状况就是,我点一下,等个一两秒那个框才真正被勾上。我相信用户在这一秒内确定已经关掉页面了。web

若是对React很熟悉的人,确定知道一个生命周期的Hook,就是shouldComponentUpdate(nextProps, nextState)。这个API就是用来决定该组件是否从新Render。因此咱们确定很开心的说,只要属性的checked值不变,就不渲染呗!算法

// return true时,进行渲染;false时,不渲染 shouldComponentUpdate(nextProps, nextState){ if(this.props.data.checked !== nextProps.data.checked){ return true; } return false; }

就这么简单么~我保存编译JSX后,火烧眉毛的刷新浏览器看一看了。一按
嗯,呵呵,组件都不会渲染了...那说明this.props.datanextProps.data的数据是一致的,这怎么可能?!我明明是经过父组件的函数修改了数组而后从新setState的呀!编程

修改数组......嗯,当时我就意识到这确定又和引用类型有关。我相信你们既然能看到这里,相信基础都是有的,就是数据的基本类型和引用类型的差异,可是我仍是乐意再用代码展现一次。

// 基本类型,number boolean string undefined null var a = 10; var b = a; a = 12; console.log(b) // => 10 // 引用类型,Object Function Array var a = [{checked: false}, {checked: true}]; var b = a; a[0].checked = true; console.log(b) // => [{checked: true}, {checked: true}] 

咱们明显能够看到它们的差异,咱们这里着重注意一下引用类型。由于变量再也不直接存值,而是变成了存指针。因此咱们的每一次都同一个指针所指内存进行修改时,都会影响到拥有该指针的变量。这里固然a和b都是指的同一个对象,因此他们修改的数据也一样是同步的。

对,咱们的this.props.datanextProps.data指的是同一个东西,因此任何修改都不会让它们区分开。那这样咱们是否是就要开始考虑如何进行深拷贝?

## 深拷贝表示只是路过打个酱油

咱们在开发过程当中,既能够享受到使用引用类型的特色带来的便利,可是同时也会忍受到很是多稀奇古怪的问题,总而言之,弊大于利。

思路其实就是将一个引用类型经过递归的方式,逐层向下取最小的基本类型,而后拼装成同样的引用类型。一看就是耗性能的主啊!若是真有这个深拷贝需求的同窗,这里推荐的是lodash库的_.cloneDeep方法,它是据我所知最完善的深拷贝方法。

固然若是你的引用类型并不复杂,例如没有函数或正则,只包含扁平化的数据时,我这里推荐一个奇淫巧计。

var newData = JSON.parse(JSON.stringify(data));

其实在咱们此次这个案例里,就很是适合这个JSON序列化后再反序列化的方法,由于咱们的数据其实也就是扁平化的。咱们把它放到函数内看一下效果。

toggleChecked(event){ let checked = event.target.checked; let index = event.target.getAttribute("data-index"); let list = JSON.parse(JSON.stringify(this.state.list)); list[index].checked = checked; this.setState({list}); },

这个世界瞬间清爽多了。可是咱们知道,在真正的开发过程当中,不必定能够用这种奇淫巧计的,那咱们除了实在没办法耗性能的deepClone,咱们还能怎么办?怎么办!?

## Immutable Data

Facebook自家有一个专门处理不可变数据的库,immutable-js。咱们知道,React实际上是很是接近函数式编程的思想的,咱们能够用下面这个式子来表示React的渲染。

UI = fRender(state, props);

Immutable Data(不可变数据)的思想就是,不存在指向同一地址的变量,全部对Immutable Data的改变,最终都会返回一份新复制的数据,各自的数据并不会互相影响。在构建大型应用时,应该很是注意这样的数据独立性,否则你连数据在哪儿被改了你或许都不知道。那说了这么多它的概念,实际使用的时候是怎么样的?

// 这段代码能够直接在Immutable的文档页面的控制台执行 var arr = Immutable.fromJS([1]); var arr1 = arr.push(2); console.log(arr.toJS(), arr1.toJS()); // => [1], [1,2] 

咱们执行后,确实原有的数据已经不可变了,又新生成了一个新的不可变数据,其实这里有个很是有趣的应用场景就是撤销。不用再担忧引用类型数据的变化,由于一切数据都被你把控了。

我相信有人确定好奇说,我每一次操做数据时都deepClone一下,也能够达到这种效果呀,这里的实现有什么不同吗?deepClone是经过递归对象进行数据的拷贝,而Immutable数据的实现则是仅仅拷贝父节点,而其余不受影响的数据节点都是共享的用同一份数据,以大大提高性能。咱们须要作的仅仅是将原生的数据转化成Immutable数据。

我知道仅仅经过语言是很难生动表现出来的,因此找到几幅图来进行解释。


咱们须要修改某个节点的数据,这个节点用黄色标了出来。

按照咱们刚才所说的,仅对父节点进行一次数据的拷贝,咱们把全新的数据拉出来,拷贝的是绿色的节点。

而其余的节点数据其实并不受影响,因此咱们能够直接使用他们的内存地址,共享一份数据。共享的数据,咱们用橙色标出。

最后咱们以最优的性能获得了一份全新的数据。


当咱们在shouldComponentUpdate里判断是否更新时,变化的数据是新的引用,而不变的数据是原来的引用,这样咱们就能够很是轻松的判断新旧数据的差别,从而大大提高性能。那咱们知道了这个Immutable能够很好的解决咱们的痛点以后,咱们该如何使用到咱们的实际项目中呢?其实很简单的,就是数据初始化时,就让它变成Immutable数据,而后以后对数据的操做就能够参照一下文档了,这里我直接重写了demo,其实也就是把取值和赋值作个改变,我会用注释标识出来。

/** * Created by YikaJ on 15/9/17. */ 'use strict'; var React = require('react'); var Immutable = require('immutable'); var App = React.createClass({ getInitialState(){ return { // 这里将传入的数据转化成Immutable数据 list: Immutable.fromJS(this.props.dataArr) } }, // 对数据的状态进行变动 toggleChecked(event){ let checked = event.target.checked; let index = event.target.getAttribute("data-index"); // 这里再也不是直接修改对象的checked的值了,而是经过setIn,从而得到一个新的list数据 let list = this.state.list.setIn([index, "checked"], checked); this.setState({list}); }, render(){ return ( <ul> {this.state.list.map((data, index)=>{ return ( <ListItem data={data} index={index} key={index} toggleChecked={this.toggleChecked} /> ) })} </ul> ) } }); // 表明每个子组件 var ListItem = React.createClass({ shouldComponentUpdate(nextProps){ // 这里直接对传入的data进行检测,由于只须要检测它们的引用是否一致便可,因此并不影响性能。 return this.props.data !== nextProps.data; }, render(){ let data = this.props.data; let index = this.props.index; // 取值也再也不是直接.出来,而是经过get或者getIn return ( <li> <input type="checkbox" data-index={index} checked={data.get("checked")} onChange={this.props.toggleChecked}/> <span>{data.get("name")}</span> </li> ) } }); // 构造一个2000个数据的数组 let dataArr = []; for(let i = 0; i < 2000; i++){ let checked = Math.random() < 0.5; dataArr.push({ name: i, checked }); } React.render(<App dataArr={dataArr}/>, document.body); 

就这样,咱们很是优雅的解决了引用类型带来的问题。其实Immutable的功能并不仅这些。它内部提供了很是多种的数据结构以供使用,例如和ES6一致的Set,这种特殊的数组不会存有相同的值。相信利用好不一样的数据结构,会很是有利于你构建复杂应用。

##PureRenderMixin表示也要来打个酱油

这里插多个React.addons内添加的东西,在我一开始探索这些性能相关问题的时候,我就注意到了这个东西。它会自行为该组件增添shouldComponentUpdate,对现有的子组件的state和props进行判断。可是它只支持基本类型的浅度比较,因此实际开发时并不能直接拿来使用。可是!咱们一旦使用了Immutable数据后,比较是不是同一指针这样的事情,天然就是浅比较,因此换句话而言,咱们可使用PureRenderMixin配合上Immutable,很是优雅的实现性能提高,并且咱们也不用再手动去shouldComponentUpdate进行判断。

var React = require("react/addons"); var ListItem = React.createClass({ mixins: [React.addons.PureRenderMixin], // .....如下代码省略 });

##总结

我相信此次提供的方法,已经能够很是优雅的解决绝大部分的性能问题了。但若是还不行,那么你可能要对你的业务逻辑代码进行优化了。下一篇,我将会介绍一下React-hot-loader这一开发神器,它能够利用webpack的模块热插拔的特性,实时对浏览器的js进行无刷新的更新,很是的酷炫!我在配置它的过程当中也摸了一些坑,因此但愿能帮助你们跳过这个坑。相信若是能好好使用它,将会大大提高你们的开发效率。

相关文章
相关标签/搜索