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

前言


来到腾讯实习后,有幸八月份开始了腾讯办公助手PC端的开发。因为办公助手主推的是移动端,所以导师也是大胆的让我们实习生来技术选型并开发,他来做code review。之前也学习过React,当然也是非常合适这一次的开发。

我会梳理这一个月来,自己对架构的思考过程和踩过的坑。当然这一切都不一定是最佳的,所以希望能有更多的建议和讨论。

例子所需库:Webpack、React、Immutable。其中Webpack用于前端构建,如果不清楚的同学可以看这里:webpack前端构建体验

出现场景


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

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

在这里我希望大家知道有一点就是,当父组件的状态state发生变化时,传入state的子组件都会进行重新渲染。

下面我们来模拟一下这种情况,一起来看看。

/**
* 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个子组件重新渲染的情况。所以表现出来的情况就是,我点一下,等个一两秒那个框才真正被勾上。我相信用户在这一秒内肯定已经关掉页面了。

如果对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进行无刷新的更新,非常的酷炫!我在配置它的过程中也摸了一些坑,所以希望能帮助大家跳过这个坑。相信如果能好好使用它,将会大大提升大家的开发效率。

React爬坑秘籍(一)——提升渲染性能的更多相关文章

  1. openGL 提升渲染性能 之 顶点数组 VBO IBO VAO

    使用openGL图形库绘制,都需要通过openGL接口向图像显卡提交顶点数据,显卡根据提交的数据绘制出相应的图形. openGL绘制方式有:直接模式,显示列表,顶点数组,顶点索引. 直接模式:最简单, ...

  2. react爬坑之路(一)--报错output.path不是绝对路径

    之前,一直在纠结是学习angular好,学习vue好,还是学习react好,网上一搜索,也是各种对比,各种互喷,看过之后更纠结.就跟小时候一样纠结长大了是上清华好,还是上北大好,最后证明我想多了.总之 ...

  3. react 爬坑记录

    1.父子组件优化其一发生render条件:数据改变(state或者props改变),有时子组件会过多render.这时可在子组件里面的生命周期钩子里执行 shouldComponentUpdate(n ...

  4. 3D硬件加速提升动画性能 与 z-index属性

    目录 1. chrome Layer borders 2. 层创建标准 3. 例子 总结 1. chrome Layer borders <WebKit技术内幕>第二章介绍了网页的结构,其 ...

  5. react+redux渲染性能优化原理

    大家都知道,react的一个痛点就是非父子关系的组件之间的通信,其官方文档对此也并不避讳: For communication between two components that don't ha ...

  6. 使用CSS3开启GPU硬件加速提升网站动画渲染性能

    遇到的问题: 网站本身设计初衷就没有打算支持IE8及以下版本浏览器,并不是因为代码兼容性问题,而是真的不想迁就那些懒得更新自己操作系统和浏览器的用户,毕竟是我自己的网站,所以我说了算!哈哈~ 没有了低 ...

  7. 微信小程序爬坑日记

    新公司上手小程序.30天,从入门到现在,还没放弃... 虽然小程序发布出来快一年了,爬坑的兄弟们大多把坑都踩平了.而我一直停留在"Hello World"的学习阶段.一来没项目,只 ...

  8. 安卓易学,爬坑不易——腾讯老司机的RecyclerView局部刷新爬坑之路

    针对手游的性能优化,腾讯WeTest平台的Cube工具提供了基本所有相关指标的检测,为手游进行最高效和准确的测试服务,不断改善玩家的体验.目前功能还在免费开放中. 点击地址:http://wetest ...

  9. android app性能优化大汇总(UI渲染性能优化)

    UI性能测试 性能优化都需要有一个目标,UI的性能优化也是一样.你可能会觉得“我的app加载很快”很重要,但我们还需要了解终端用户的期望,是否可以去量化这些期望呢?我们可以从人机交互心理学的角度来考虑 ...

随机推荐

  1. TryUpdateModel方法 模型绑定

    文档资料:https://msdn.microsoft.com/zh-cn/library/ee728634.aspx 有很多重载其中 Controller.TryUpdateModel<TMo ...

  2. Kubernetes实践--hello world 示例

    本文所说的Hello world是一个web留言板应用,并且是基于PHP+Redis的两层分布式架构的web应用,前端PHP web网站通过访问后端Redis数据库完成用户留言的查询和添加功能,具备读 ...

  3. 浅谈jsonp

    要谈jsonp,首先要弄明白jsonp是什么,它是用来干嘛的.jsonp其实就是我们常用的script标签,用来解决跨域的,只不过这个标签是动态创建的,为啥要动态创建涅. 举个小栗子: 假如我们远程文 ...

  4. SpringBoot 使用 EhCache2.x 缓存(三十一)

    SpringBoot 使用 EhCache2.x 缓存入门很简单,废话少说上干货: 1.在POM.xml中增加jar包 <!--开启 cache 缓存--> <dependency& ...

  5. 005PHP文件处理——目录操作,统计大小 filesize unlink

    <?php /* 目录操作,统计大小 filesize unlink * */ $dir = dir("."); while (($file = $dir->read( ...

  6. 001——数组(一)数组知识及foreach函数应用

    <?php /**数组(一)数组知识及foreach函数应用*/ /*数组:在一个变量中,存储一个或多个值,每一个元素都有一个访问ID * * */ /* * //索引型数组 $arr=arra ...

  7. UVALive 4639 && SPOJ SPOINTS && POJ 3805 && AOJ 1298 Separate Points 求两个凸包是否相交 难度:3

    https://icpcarchive.ecs.baylor.edu/index.php?option=com_onlinejudge&Itemid=8&page=show_probl ...

  8. 最终还是迁移到github

    作为全球最大的程序员同性交友社区,github pages 吸引了我 为了有一个更好的博客的写作环境 将会把内容逐渐迁移到 github.io 地址 zongxiao.github.io 新的文章也会 ...

  9. HDU 3746

    http://acm.hdu.edu.cn/showproblem.php?pid=3746 kmp的Nxet数组求字符串循环节例题 lenB%(lenB-Next[lenB])==0则其有周期len ...

  10. 数据库数据——>文件xml

    xml文件格式 <smss> <sms> <data> </data> </sms> </smss> 这里面的意思是将数据库里面 ...