React + Reflux 渲染性能优化原理


React
React的优点有很多,现在很多应用都接入React这个框架。
在我看来,有下列优点:
Facebook团队研发并维护——有团队维护更新且有质量保证
在MVVM结构下只起View的作用——简单接入,不需要花费大量人力重构代码
组件化形式构建Web应用——复用性强,提高开发效率
用Virtual DOM减少对DOM的频繁操作提高页面性能——批量操作减少重排(reflows)和重绘(repaints)次数——性能对比旧的方式有提高

React对重排和重绘的提高
雅虎性能优化比较重要的点,老司机自行忽略。
如下图,HTML被浏览器解析为DOM树,CSS代码加载进来解析为样式结构体,两者关联组成渲染树,之后浏览器把渲染树绘制出来就是我们看到的网页了。这里如果我们对DOM树或者样式结构体做一些操作,如删除某个节点,样式改为隐藏(display:none)等等,会触发重排进而导致重绘。


触发重排的条件
DOM元素的数量属性变化
DOM树的结构变化——节点的增减、移动
某些布局属性的读取和设置触发重排——offsetTop/offsetWidth/scrollTop等等导致子级、后续兄弟元素、父节点因重新计算布局而重排

触发重绘的条件
简单样式属性的变化——颜色、背景色等
重排导致的重绘

而React维护了一个Virtual DOM将短时间的操作合并起来一起同步到DOM,所以这也是它对整个前端领域提出的最重要的改变。
为什么引入Reflux?
上面说了React在MVVM结构下只起View的作用,那么除了View,MVVM下还有Model,ViewModel。
而纯粹的View,会让整个逻辑耦合在一层下,数据也需要层层传递,不方便控制和复用。


故业内也有一堆的分层框架——如最早的flux,现在部门在用的Reflux,以及Redux。
对比Redux,Reflux更容易理解和上手——这也是现状,学习成本越低,接入现有业务就越容易。
Reflux
reflux的架构非常简单,就是三部分
Action 理解为一个命令或者动作,通过它来向组件发出”指令”
Store 为ViewModel部分,组件的一些状态属性会存储在这里
View Component 为组件模板

所以Reflux只是让我们,更好的去操作组件,通过一个Action命令,叫组件去干嘛,组件自己通过写好的代码,对命令做出反应(变化为不同的state状态)。
React+Reflux起到的作用
现在你已经有了两个小工具了,写一个组件,通过Action调用组件就可以了。
写到这里,你应该能体会到,所有的引入就是为了让代码写起来更有效率,更易用,复用性更强。
Pure Component
纯净的组件:在给定相同props和state的情况下会渲染出同样结果
其优点有这么几点:
我们写的组件都应该是只依赖props和state的,而不应该依赖其他全局变量或参数
纯净的组件方便复用、测试和维护

组件生命周期
React组件有两部分
第一部分是初始化的生命周期:
getDefaultProps
geInitialState
componentWillMount
render
componentDidMount

第二部分是被action触发,需要更新:
shouldComponentUpdate
componentWillUpdate
render
conponentDidUpdate

shouldComponentUpdate
shouldComponentUpdate这个方法可以说是一个预留的插入接口。
在上面更新的时候,第一步就是调用的这个方法判断组件是否该被重新渲染。
shouldComponentUpdate是在React组件更新的生命周期中,用于判断组件是否需要重新渲染的一个接口,它有两个返回值:
返回true,则进入React的Virtual DOM比较过程

返回false,则跳过Virtual DOM比较与渲染等过程


如上图,这是一棵React Virtual DOM的树。
C1在 ShouldComponentUpdate 返回了true,即默认值,代表需要更新,进入 Virtual DOM Diff 过程,返回false,不相同,需要更新
C2在 ShouldComponentUpdate 返回了false,不再更新,C4,C5因为被父节点在 ShouldComponentUpdate 中返回了false,所以不再更新
C3在 ShouldComponentUpdate 返回了true进入 Virtual DOM Diff 过程,比对结果为false,新旧不一样,需要更新
轮到C6, ShouldComponentUpdate 返回了true,进入 Virtual DOM Diff 的过程,返回了false,即新旧两个节点不相同,所以这个节点需要更新
C7在 ShouldComponentUpdate 返回了false,即不需要更新,节点不变
C8在 ShouldComponentUpdate 返回了true,进入 Virtual DOM Diff 比对过程,结果为true,新旧相等,不更新

大概就是这么一个过程,在这里,Diff算法其实还是比较复杂的,比较好的做法是我们来写入ShouldComponentUpdate来自己控制组件的更新,而不是依赖React帮我们做比较。
进入正文
前面讲了那么多,相信懂React的都懂了,就不再详细讲了,Diff算法有兴趣的可以自己去翻源码,网上也有一堆模拟实现的例子。
接下来介绍一个探索reflux&react渲染优化的例子。
这里试图,模拟一个比较现实的例子,抛开很多业务代码,让问题变得直接。
首先例子有三个组件,两个按钮,5个数字,还有一个重复打印文本的大组件。
1basicDemo 是没有优化的例子,每50ms会发出action更改store数据触发渲染
2perfDemo 使用addons插件Perf分析页面性能的例子
3pureRenderMixinDemo 使用addons插件pureRenderMixin优化页面性能的例子
4updateDemo 使用了addons插件update优化页面性能的例子
5immutableDemo 使用了 Immutable.js 优化页面性能的例子

说明
gulpfile.js为gulp构建代码,会将 tpl.js 的JSX代码翻译为js代码,需要的可以自己修改,每次转化模板需要 gulp 运行一下
modulejs模块加载器和myView单页SPA框架为腾讯通讯与彩票业务部前端团队这边的基本框架,具体的请戳 这里 查看
需要关注的文件index.html 页面入口,规定了执行的模块
app.js 应用程序入口
todoAction.js (reflux架构下,demo的action)
todoStore.js (reflux架构下,demo的store)
tpl.js 组件的jsx文件

简单用法
cd ./xxx/ (这里的xxx为上面对应的 ……./4updateDemo/ 目录)
http-server -p 8888 端口可以自定义,http-server模块已在 node_module 目录下,担心版本依赖问题,已上传 node_module 目录,直接打开就可以了
打开浏览器便可浏览,详情请看控制台

1.basicDemo
1basicDemo目录是一个最原始的目录,这里你可以看到我们哪里出现了问题。
cd ./example 打开这个没优化过的例子的目录
http-server -p xxxx 这里端口随意,不冲突就好
浏览器访问并打开控制台,会看到
5 tpl.js:32 createNum组件被更新了 tpl.js:10 TextComponent被更新了2 tpl.js:57 createBtn组件被更新了

初始化createNum组件被渲染了5次,因为有5个,createBtn组件被渲染了两次,因为有点击开始和点击结束两个按钮。通过不同的传参而改变形态。
点击开始会触发 action ,让store的数据每次+1,点击结束会清除定时器
点击开始可以看到 控制台的数据每次都会刷新整个界面的所有组件,特别是有一个大组件 TextComponent ,是重复5000次文本的,每次重新渲染就有很多的损耗。这就是我们要优化的地方——减少某些 关键部分 的重新渲染的次数,减少无用对比的消耗
这里你可以打开Chrome控制台的Timeline来看一下,点击开始,打开Timeline面板,每1S左右会有一个脚本执行的高峰期。
我们知道特别是在移动端,CPU和内存的资源显得尤为稀缺(大概只能占用正常CPU和内存的10%,微信手Q等可能会因为友商系统对应用程序的优先级设计使这个限制略有提高——我说的就是小米哈哈哈),所以这样说来,性能这一块在移动手机web显得非常非常重要。


2.Perl
Perl是react-addons带来的性能分析工具,这里的perfDemo是结合Chrome插件的例子。
要向全局暴露一个 window.Perl 变量,然后就可以愉快的配合Chrome插件使用了
React-addons插件版本的 Perf插件 提供原生的API——用在首次渲染部分
Chrome插件 ——用在有交互的部分
console tool ——需要查看对比新旧值的情况下

这里的wasted time就是在做属性没变化的重复渲染的过程,可以优化。
用法与Chrome开发工具的TimeLine用法类似,点击start开始记录,后点击stop结束


3.PureRenderMixin
一个简单的通用优化工具,通过 浅对比(shallowCompare) 方法对比新旧两个组件的状态,达到减少重复渲染的目的。
注意这里组件的store必须无关联,原因是shallowCompare的时候,比较的是组件关联的store的数据,而例子里面store是一个,其他组件num的变化也会引起这里TextComponent组件的更新
这里将store与顶级组件APP关联起来,然后在子孙组件下自定采用props传递的方式处理(传递基本类型的数据),这样就可以让pureRenderMixin的通用化了,唯一的缺点是,传递props要控制,只把组件需要的属性传递下去,这里会比较麻烦,但是这样又是性能较高又比较好理解的处理方式(相对其他要拷贝属性的方式)
*store下,option里面的对象,受pureRenderMixin的限制,不可以出现引用类型
PureRenderMixin其实是封装了更底层的shallowCompare接口的
简单用法如下:
var PureRenderMixin = require('react').addons.PureRenderMixin;React.createClass({ mixins: [PureRenderMixin], render: function() { return <div className={this.props.className}>foo</div>; }});

就加了一个mixins,看起来简单优雅有木有。可以在众多组件里面copy通用啊有木有
那这里干了什么?

  React.createClass({
     mixins: [PureRenderMixin], render: function() {
     return <div className={this.props.className}>foo</div>; 
}});```

```var ReactComponentWithPureRenderMixin = {
 //帮你写了一个shouldComponentUpdate方法 
    shouldComponentUpdate: function (nextProps, nextState) {
         return shallowCompare(this, nextProps, nextState);
 }};```

```function shallowCompare(instance, nextProps, nextState) { 
//分别比较props和state属性是否相等 
          return !shallowEqual(instance.props, nextProps) || !shallowEqual(instance.state, nextState);
}```

```function shallowEqual(objA, objB) {
 if (objA === objB) { 
//store嵌套层级太深这里就会返回true,引用类型内存指向同一空间 return true; 
} 
if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) {
 return false; } var keysA = Object.keys(objA); var keysB = Object.keys(objB); 
if (keysA.length !== keysB.length) {
 return false; 
} // Test for A's keys different from B. var bHasOwnProperty = hasOwnProperty.bind(objB); for (var i = 0; i < keysA.length; i++) {
 if (!bHasOwnProperty(keysA[i]) || objA[keysA[i]] !== objB[keysA[i]]) {
 return false; 
} }
 return true;
}```

所以PureRenderMixin这个插件,只能比较state和props为基本类型的部分。
如果有更加深层次的store数据嵌套,就要借助于update插件或者Immutablejs来深拷贝store的数据另存一份了。
![](http://upload-images.jianshu.io/upload_images/2830117-b500a6020ce91f8d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
**4.用update优化(也称Immutable Helper)**
update是addons里面的一个方法,旨在对拷贝对象复杂的过程来做一些语法上的优化,具体可以看 [react官方文档](https://facebook.github.io/react/docs/update.html)
//extend复制对象属性的时候var newData = extend(myData, { x: extend(myData.x, { y: extend(myData.x.y, {z: 7}), }), a: extend(myData.a, {b: myData.a.b.concat(9)})});

//用update的时候,提供了一些语法糖让你不用写那么多var update = require('react-addons-update');var newData = update(myData, { x: {y: {z: {$set: 7}}}, a: {b: {$push: [9]}}});

cd ./updateDemo 打开这个用addons.update优化过的例子的目录
http-server -p xxxx 这里端口随意,不冲突就好
这个例子与上面一个例子唯一的不同是这里用了 **addons.update来进行store数据的复制** ,具体的可以看todoStore和tpl这两个模块的代码,其他基本无修改
这里update是参考了MongoDB’s query的部分语法,具体的可以看 [这里](http://docs.mongodb.org/manual/core/crud-introduction/#query) ,类比数组方法,返回一个新的实例。
{$push: array} 类似数组的push方法
{$unshift: array} 类似数组的unshift方法
{$splice: array of arrays} 类似数组的splice方法
{$set: any} 整个替换目标
{$merge: object} 合并目标和object的 keys.
{$apply: function} 传递当前的值给 function 并用返回值更新它

但是由Timeline的观察来看,复制对象属性的性能远比刷新一个大组件的性能高。
![](http://upload-images.jianshu.io/upload_images/2830117-0296f77fc80c40a2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
**5.Immutablejs**
Immutable.js是Facebook为解决数据持久化而独立出来的一个库,传统的,比如我们有
var a = {b:1};function test(obj){ obj.b = 10; return obj;}test(a); //10

函数对对象的操作,你不会知道这个函数对对象进行了什么操作。也就是说是封闭的。
而Immutable每次对对象的操作都会返回一个新对象
Immutable.js提供了7种不可变的数据类型: List Map Stack OrderedMap Set OrderedSet Record ,对Immutable对象的操作均会返回新的对象,例如:
var obj = {count: 1};var map = Immutable.fromJS(obj);var map2 = map.set('count', 2);console.log(map.get('count')); // 1console.log(map2.get('count')); // 2

引入Immutable.js,需要对现有的业务代码进行改动,通常是对tpl和store两部分进行操作,初始化数据的时候生成一个Immutable的数据类型,之后每次get,set操作都会返回一个共享的新的对象。
50ms渲染一次,重复渲染200次的截图,引入了immutable用了其set方法:
![](http://upload-images.jianshu.io/upload_images/2830117-921aa466960fa51c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
50ms渲染一次,重复渲染200次的截图,引入了immutable用了其update方法:
![](http://upload-images.jianshu.io/upload_images/2830117-531b7692760e7be2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
**6.seamless-immutable && Observejs**
一个是immutable的阉割版,一个是AlloyTeam推的。
两者都是通过Object.defineProperty(IE9+)对set和get操作进行处理,优点是文件比较小。
**7.写在最后**
自己设想,组件化运用到极致,应该是像 微信weui 那样
有一套非常适合接入,复用性非常强的组件库。拿来就用,不需要再次开发
应该兼顾起上面说的减少重复渲染的部分
开发友好

这里也思考一些可能做到的变化:
将一个组件的action/store/JSX/样式代码Style 写在 **一个文件** 里,这样方便修改和调用,封闭组件内部实现细节,对外只暴露action操作和store的一些get方法,这样可以修改或者是获取到组件的某些现在时刻的属性(也有同学是直接封装为一个对象,通过对象暴露其store,action)
组件共享或依赖的数据,应在公共父级的store或独立成一个单独的部分,然后采用props传递的形式或从独立的store里面取数据
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容