React本身就非常关注性能,其提供的虚拟DOM搭配上Diff算法,实现对DOM操作最小粒度的改变也是非常的高效。然而其组件渲染机制,也决定了在对组件进行更新时还可以进行更细致的优化。
react的组件渲染分为初始化渲染和更新渲染。
在初始化渲染的时候会调用根组件下的所有组件的render方法进行渲染,如下图(绿色表示已渲染,这一层是没有问题的):
但是当我们要更新某个子组件的时候,如下图的绿色组件(从根组件传递下来应用在绿色组件上的数据发生改变):
我们的理想状态是只调用关键路径上组件的render,如下图:
但是react的默认做法是调用所有组件的render,再对生成的虚拟DOM进行对比,如不变则不进行更新。这样的render和虚拟DOM的对比明显是在浪费,如下图(黄色表示浪费的render和虚拟DOM对比)
那么如何避免发生这个浪费问题呢,这就要牵出我们的shouldComponentUpdate
react在每个组件生命周期更新的时候都会调用一个shouldComponentUpdate(nextProps, nextState)
函数。它的职责就是返回true或false,true表示需要更新,false表示不需要,默认返回为true,即便你没有显示地定义 shouldComponentUpdate 函数。这就不难解释上面发生的资源浪费了。
为了进一步说明问题,我们再引用一张官网的图来解释,如下图( SCU表示shouldComponentUpdate,绿色表示返回true(需要更新),红色表示返回false(不需要更新);vDOMEq表示虚拟DOM比对,绿色表示一致(不需要更新),红色表示发生改变(需要更新)):
根据渲染流程,首先会判断shouldComponentUpdate(SCU)是否需要更新。如果需要更新,则调用组件的render生成新的虚拟DOM,然后再与旧的虚拟DOM对比(vDOMEq),如果对比一致就不更新,如果对比不同,则根据最小粒度改变去更新DOM;如果SCU不需要更新,则直接保持不变,同时其子元素也保持不变。
为了避免一定程度的浪费,react官方还在0.14版本中加入了无状态组件,如下:
// es5
function HelloMessage(props) {
return <div>Hello {props.name}</div>;
}
// es6
const HelloMessage = (props) => <div>Hello {props.name}</div>;
具体可参考官网:Reusable Components
既然明白了这关键所在,现在是时候向我们的大大小小一箩筐组件开刀了。
下面我们以音量图标为例,这是一个svg图标,不需要更新,所以直接return false
import React, {Component} from 'react';
class Mic extends Component {
constructor(props) {
super(props);
}
shouldComponentUpdate() {
return false;
}
render() {
return (
<svg className="icon-svg icon-mic" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" aria-labelledby="title">
<title>mic</title>
<path className="path1" d="M15 22c2.761 0 5-2.239 5-5v-12c0-2.761-2.239-5-5-5s-5 2.239-5 5v12c0 2.761 2.239 5 5 5zM22 14v3c0 3.866-3.134 7-7 7s-7-3.134-7-7v-3h-2v3c0 4.632 3.5 8.447 8 8.944v4.056h-4v2h10v-2h-4v-4.056c4.5-0.497 8-4.312 8-8.944v-3h-2z"></path>
</svg>
)
}
}
export default Mic;
先来个官网的例子,通过判断id是否改变来确定是否需要更新:
shouldComponentUpdate: function(nextProps, nextState) {
return nextProps.id !== this.props.id;
}
看起来也没那么玄乎,直接一个!==
对比下就ok了,那是不是所有的都可以这样直接对比就可以呢? 我们先来看下js的两个数据类型(原始类型与引用类型)的各自比较
// 原始类型
var a = 'hello the';
var b = a;
b = b + 'world';
console.log(a === b); // false
// 引用类型
var c = ['hello', 'the'];
var d = c;
d.push('world');
console.log(c === d); // true
我们可以看到a和b不等,但是c和d是一样一样的,这修改了d,也直接修改了c,那还怎么对比(关于原始类型与引用类型的区别这里就不说明了)。
现在看来我们得分情况处理了,原始类型数据和引用类型数据得采用不同的办法处理。
这没什么好说的,直接比对就是了。但是每个人都是想偷懒的,这要是每个组件都要这样去写下也挺麻烦的,于是react官方有了插件帮我们搞定这事:
var PureRenderMixin = require('react-addons-pure-render-mixin');
React.createClass({
mixins: [PureRenderMixin],
render: function() {
return <div className={this.props.className}>foo</div>;
}
});
var shallowCompare = require('react-addons-shallow-compare');
export class SampleComponent extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
return shallowCompare(this, nextProps, nextState);
}
render() {
return <div className={this.props.className}>foo</div>;
}
}
既然引用类型数据一直返回true,那就得想办法处理,能不能把前后的数据变成不一样的引用呢,那样不就不相等了吗?于是就有了我们的不可变数据。
var update = require('react-addons-update');
var newData = update(myData, {
x: {y: {z: {$set: 7}}},
a: {b: {$push: [9]}}
});
这样newData与myData就可以对比了。
其API如下:
const newValue = {
...oldValue
// 在这里做你想要的修改
};
// 快速检查 —— 只要检查引用
newValue === oldValue; // false
// 如果你愿意也可以用 Object.assign 语法
const newValue2 = Object.assign({}, oldValue);
newValue2 === oldValue; // false
然后在shouldComponentUpdate
中进行比对
shouldComponentUpdate(nextProps) {
return isObjectEqual(this.props, nextProps);
}
const isObjectEqual = (obj1, obj2) => {
if(!isObject(obj1) || !isObject(obj2)) {
return false;
}
// 引用是否相同
if(obj1 === obj2) {
return true;
}
// 它们包含的键名是否一致?
const item1Keys = Object.keys(obj1).sort();
const item2Keys = Object.keys(obj2).sort();
if(!isArrayEqual(item1Keys, item2Keys)) {
return false;
}
// 属性所对应的每一个对象是否具有相同的引用?
return item2Keys.every(key => {
const value = obj1[key];
const nextValue = obj2[key];
if(value === nextValue) {
return true;
}
// 数组例外,再检查一个层级的深度
return Array.isArray(value) &&
Array.isArray(nextValue) &&
isArrayEqual(value, nextValue);
});
};
const isArrayEqual = (array1 = [], array2 = []) => {
if(array1 === array2) {
return true;
}
// 检查一个层级深度
return array1.length === array2.length &&
array1.every((item, index) => item === array2[index]);
};
我们目前采用的是在reducer里面更新数据使用Object.assign({}, state, {newkey: newValue}
(数据管理采用redux),然后在组件里面根据某个具体的字段判断是否更新,如title或id等,而不是判断整个对象:
shouldComponentUpdate: function(nextProps, nextState){
return nextProps.title !== this.props.title;
}
(表示这个js太大了,所以我也没有具体实践过。)
Immutable Data 就是一旦创建,就不能再被更改的数据。对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象。
Immutable 实现的原理是 Persistent Data Structure(持久化数据结构),也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。同时为了避免 deepCopy 把所有节点都复制一遍带来的性能损耗,Immutable 使用了 Structural Sharing(结构共享),即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。请看下面动画:
具体如何使用可参考下面两篇文章:
至此,shouldComponentUpdate
优化介绍完毕,我们接着进入另一个需要的优化点:列表类组件
列表类组件默认更新方式会比较复杂(因为可能会涉及到增删改,排序等复杂操作),所以需要加上一个key属性,提供一种除组件类之外的识别一个组件的方法。
如果某个组件key值发生变化,React会直接跳过DOM diff,重新渲染,从而节省计算提高性能。
key值除了告诉React什么时候抛弃diff直接重新渲染之外,更多的情况下可用于列表顺序发生改变的时候(如删除某项,插入某项,数据某个特定字段顺序或倒序显示),可以根据key值的位置直接调整DOM顺序。
如下例,根据时间排序图片(没有key值):
var items = sortBy(this.state.sortingTime, this.props.items);
return items.map(function(item) {
return <img src={item.src} />;
})
如果顺序发生改变,React会对元素进行diff操作并确定出最高效的操作是改变其中几个img元素的src属性。虽然如此,但是还是有了diff的计算时间,效率其实已经非常低了。
而如果加上key值之后
return <img src={item.src} key={item.id} />;
React得出的结论就不是diff,而是直接使用insertBefore操作,而这个操作是移动DOM节点最高效的办法。
同理如果有一老师批改的作业列表,在批改完某个作业之后,该作业item应该被移除,有了key值之后,一检查key值,发现少了一个,于是直接移除该dom节点。
需要注意的是:每个key值是唯一的,在组件内部也不能通过this.props.key
获取到。
现在我们知道了如何去优化react的组件,但是优化不能光靠自己的直觉,那么有没有个什么工具可以告诉我们什么时候需要优化呢?
react官方提供一个插件React.addons.Perf可以帮助我们分析组件的性能,以确定是否需要优化。
下面简单说下如何使用:
react-addons-perf
import Perf from 'react-addons-perf';
打开console面板,先输入Perf.start()
执行一些组件操作,引起数据变动,组件更新,然后输入Perf.stop()
。(建议一次只执行一个操作,好进行分析)
再输入Perf.printInclusive
查看所有涉及到的组件render,如下图(官方图片):
或者输入Perf.printWasted()
查看下不需要的的浪费组件render,如下图(官方图片):
如果printWasted
有数据,则表示可以优化,优化得好,是一个空数组,没有数据。
下图是二张我截图的对比图(截图为开发环境,通过require得到react),从第一张的Perf.printWasted()
可以得到有15个浪费的render,于是我进行了一次shouldComponentUpdate
优化,得到第二张图,为空数据:
图一,没有优化前
图二,优化后
其他api可到官网查阅。