作者:ManfredHu
链接:http://www.manfredhu.com/2016/11/08/23-reactRenderingPrinciple
声明:版权所有,转载请保留本段信息,否则请不要转载

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的架构非常简单,就是三部分

  1. Action 理解为一个命令或者动作,通过它来向组件发出”指令”
  2. Store 为ViewModel部分,组件的一些状态属性会存储在这里
  3. View Component 为组件模板

所以Reflux只是让我们,更好的去操作组件,通过一个Action命令,叫组件去干嘛,组件自己通过写好的代码,对命令做出反应(变化为不同的state状态)。

React+Reflux起到的作用

现在你已经有了两个小工具了,写一个组件,通过Action调用组件就可以了。
写到这里,你应该能体会到,所有的引入就是为了让代码写起来更有效率,更易用,复用性更强。

Pure Component

纯净的组件:在给定相同props和state的情况下会渲染出同样结果
其优点有这么几点:

  1. 我们写的组件都应该是只依赖props和state的,而不应该依赖其他全局变量或参数
  2. 纯净的组件方便复用、测试和维护

组件生命周期

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文件

简单用法

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

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.addons = {
CSSTransitionGroup: ReactCSSTransitionGroup,
LinkedStateMixin: LinkedStateMixin,
PureRenderMixin: ReactComponentWithPureRenderMixin, //看这里
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的数据另存一份了。

4.用update优化(也称Immutable Helper)

update是addons里面的一个方法,旨在对拷贝对象复杂的过程来做一些语法上的优化,具体可以看react官方文档

//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的部分语法,具体的可以看这里,类比数组方法,返回一个新的实例。

  • {$push: array} 类似数组的push方法
  • {$unshift: array} 类似数组的unshift方法
  • {$splice: array of arrays} 类似数组的splice方法
  • {$set: any} 整个替换目标
  • {$merge: object} 合并目标和object的 keys.
  • {$apply: function} 传递当前的值给 function 并用返回值更新它

但是由Timeline的观察来看,复制对象属性的性能远比刷新一个大组件的性能高。

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')); // 1
console.log(map2.get('count')); // 2

引入Immutable.js,需要对现有的业务代码进行改动,通常是对tpl和store两部分进行操作,初始化数据的时候生成一个Immutable的数据类型,之后每次get,set操作都会返回一个共享的新的对象。

50ms渲染一次,重复渲染200次的截图,引入了immutable用了其set方法:

50ms渲染一次,重复渲染200次的截图,引入了immutable用了其update方法:

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里面取数据

License

源码传送门
MIT. Copyright (c) 2016 ManfredHu.

React + Reflux 渲染性能优化原理的更多相关文章

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

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

  2. 相当有用的react+redux渲染性能优化原理

    学习地址:http://foio.github.io/react-redux-performance-boost/

  3. Android性能优化之UI渲染性能优化

    版权声明:本文出自汪磊的博客,未经作者允许禁止转载. 本篇博客主要记录一些工作中常用的UI渲染性能优化及调试方法,理解这些方法对于我们编写高质量代码也是有一些帮助的,主要内容包括介绍CPU,GPU的职 ...

  4. react 实用的性能优化方式

    react 组件渲染分为初始化渲染和更新渲染,当我们更新某个组件的时候,只是想关键路径上组件的render,但react的默认做法是调用所以组件的reder,再生成虚拟dom进行对比,如不变则不进行更 ...

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

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

  6. react 首屏性能优化

    首屏优化点:1.加载包(bundle.js)文件的大小,越小,首屏渲染速度越快 (按需加载) 2.优先渲染用户直观看到的页面部分(懒加载) 技术点:react-loadable . react-laz ...

  7. reselect是怎样提升react组件渲染性能的?

    reselect是什么? reselect是配合redux使用的一款轻量型的状态选择库,目的在于当store中的state重新改变之后,使得局部未改变的状态不会因为整体的state变化而全部重新渲染, ...

  8. React + Reflux

    React + Reflux 渲染性能优化原理   作者:ManfredHu 链接:http://www.manfredhu.com/2016/11/08/23-reactRenderingPrinc ...

  9. React性能优化之PureComponent 和 memo使用分析

    前言 关于react性能优化,在react 16这个版本,官方推出fiber,在框架层面优化了react性能上面的问题.由于这个太过于庞大,我们今天围绕子自组件更新策略,从两个及其微小的方面来谈rea ...

随机推荐

  1. 微信网页动画---swiper.animate.css

    项目需要,自己写了个demo <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> ...

  2. [转载]WebStorm快捷键操作

    http://www.cnblogs.com/yangjinjin/archive/2013/01/30/2883172.html 1. ctrl + shift + n: 打开工程中的文件,目的是打 ...

  3. 悲催的IE6 七宗罪大吐槽(带解决方法)第一部分

    一.奇数宽高 悲剧的IE6啊,为何有如此多bug,但用户市场又那么大,真让我们搞网站的纠结.今天就遇到了一个非常奇怪但又很细节的一个bug,一个外部的相对定位div,内部一个绝对定位的div(righ ...

  4. .NET 下第一次接触Redis数据库

    关于Redis 1.简介 Redis是著名的NOSQL(Not Only SQL)数据库,是键值对结构.(我只用过键值对结构的) 他为存储键值对做了优化,在大型网站中应用广泛.Redis提供了数据的自 ...

  5. CodeForces 816C 思维

    On the way to school, Karen became fixated on the puzzle game on her phone! The game is played as fo ...

  6. 天梯赛 L2-024. (并查集) 部落

    题目链接 题目描述 在一个社区里,每个人都有自己的小圈子,还可能同时属于很多不同的朋友圈.我们认为朋友的朋友都算在一个部落里,于是要请你统计一下,在一个给定社区中,到底有多少个互不相交的部落?并且检查 ...

  7. HDU 1251 统计难题 (裸的字典树)

    题目链接 Problem Description Ignatius最近遇到一个难题,老师交给他很多单词(只有小写字母组成,不会有重复的单词出现),现在老师要他统计出以某个字符串为前缀的单词数量(单词本 ...

  8. 44、File类简介

    使用File类创建文件夹 File类在java.io包下,看名字应该可以猜到,这个类是跟文件夹操作有关,下面使用File类中的方法在硬盘中创建文件夹. package com.sutaoyu.file ...

  9. PE结构详解

    1 基本概念 下表描述了贯穿于本文中的一些概念: 名称 描述 地址 是“虚拟地址”而不是“物理地址”.为什么不是“物理地址”呢?因为数据在内存的位置经常在变,这样可以节省内存开支.避开错误的内存位置等 ...

  10. web性能优化之js图片懒加载

    html <div class="container"> <ul> <li> <div id="first" clas ...