React本身就非常关注性能,其提供的虚拟DOM搭配上DIff算法,实现对DOM操作最小粒度的改变也是非常高效的,然而其组件的渲染机制,也决定了在对组件更新时还可以进行更细致的优化。
react组件渲染
在讲react生命周期时,就谈到过react组件分为了初始化渲染和更新渲染, 初始化渲染会调用根组件下的所有组件的render方法进行渲染, 如下图所示(绿色表示已经渲染):
但是,当我们要更新某个组件的时候,如下面的绿色组件(从根组件传递下来应用在绿色组件上的数据发生变化)
即在这三层中,只有最底下的一层中的某个绿色组件的数据发生变化,所以理想状态就是只调用关键路径组件上的render,如下图:
但是 react 的默认做法是调用所有组件的render,再对生成的虚拟DOM进行比对,如果不变则不进行更新,这样的reader和虚拟DOM的对比是在浪费,如下图(黄色表示浪费的render和虚拟DOM的对比)
(橘黄色 = 浪费的渲染)
哦,不!我们所有的节点都被重新渲染了。
注意: 每次更新的方式是: 如果状态发生改变,就先render,render的过程是生成虚拟DOM的过程,然后在拿这个虚拟DOM和上一次的虚拟DOM进行比对,生成一个patch,通过这个patch打到真实DOM上,得到更新,虽然,通过虚拟DOM,react很好的做到了DOM的更新很少,但是,在状态发生变化的时候,react在render的时候却整个(所有的组件)都render了,而不是render一部分(最好只render状态发生变化的那个组件),这样就造成了浪费。
那么如何才能避免发生这个浪费问题呢? 这里就要迁出我们的 shouldComponentUpdate 了。
shouldComponentUpdate
在上面我们说到了在某一个组件更新的时候,只需要这一个组件调用render方法就可以了,而其他的组件不需要调用render方法(即使通过比对发现DOM没有改变,则不更新DOM,但是比对的时间也是被浪费了),那么怎么做到呢?实际上,react在每个组件生命周期更新的时候都会调用一个 shouldComponentUpdate(nextProps, nextState)函数, 他的职责就是返回true或false, true表示需要更新,而false表示不需要更新,默认返回true。 即使你没有显示的定义这个shouldComponentUpdate函数,那么返回的就是true,一定需要发生重新渲染,这样也就不安理解为什么会发生上面的资源浪费了 。
为了避免一定程度的浪费,react官方还在0.14版本中添加了无状态组件,如下所示:
// es5function HelloMessage(props) { returnHello {props.name};}
// es6const HelloMessage = (props) =>Hello {props.name};
因为无状态组件压根就没有涉及到状态的改变,那么他们是不会参与到每次render和比对虚拟DOM的过程的,而是一旦创建,就稳如泰山了。
牛刀小试! 直接把一些不需要更新的组件返回false
比如,下面时一个音量图标,这是一个svg图标,不需要更新,所以在shouldComponentUpdate中直接return false, 如下:
import React, {Component} from 'react';class Mic extends Component { constructor(props) { super(props); } shouldComponentUpdate() { return false; } render() { return ( ) }}export default Mic;
这样,无论上面的状态发生了怎样的改变,这里直接return false,所以就不会有render的过程了,避免了性能上的消耗和浪费。 其实,这样的情况直接使用无状态组件会更好,就不会有这个判断。
登堂入室,对数据进行对比确定是否需要更新
先来个官网的例子,通过判断id是否改变来确定是否需要更新:
shouldComponentUpdate: function(nextProps, nextState) { return nextProps.id !== this.props.id;}
是不是很简单呢?但是是不是所有的都可以直接这么对比呢? 我们来看下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是相等的,因为对于引用类型,比较的时地址,这里的地址并乜有发生改变! 所以,我们得分情况处理了,原始数据类型和应用类型得采用不同的方法处理。
原始数据类型:
原始数据的比对很简单,我们直接比对就是了,但是我们还可以更简单的比对, 因为每一个组件这么写下去也是挺麻烦的,于是react官方有了插件帮助我们搞定这件事:
- (es5的插件)
var PureRenderMixin = require('react-addons-pure-render-mixin');React.createClass({ mixins: [PureRenderMixin], render: function() { returnfoo; }});
- (es6的插件)
var shallowCompare = require('react-addons-shallow-compare');export class SampleComponent extends React.Component { shouldComponentUpdate(nextProps, nextState) { return shallowCompare(this, nextProps, nextState); } render() { returnfoo; }}
引用类型数据:
因为引用类型比对的是地址,所以可能一直都返回true,所以,我们得想办法吧前后的数据编程不一样的引用, 如下:
- react官方提供了一个
var update = require('react-addons-update');var newData = update(myData, { x: {y: {z: {$set: 7}}}, a: {b: {$push: [9]}}});
这样,newDate和myDate就可以比对了。
- 直接改变引用
const newValue = { ...oldValue // 在这里做你想要的修改};// 快速检查 —— 只要检查引用newValue === oldValue; // false// 如果你愿意也可以用 Object.assign 语法const newValue2 = Object.assign({}, oldValue);newValue2 === oldValue; // false
Immutable Data 就是一旦创建,就不能再被更改的数据。对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象。
Immutable 实现的原理是 Persistent Data Structure(持久化数据结构),也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变。同时为了避免 deepCopy 把所有节点都复制一遍带来的性能损耗,Immutable 使用了 Structural Sharing(结构共享),即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享, 这样,就是每次开辟了新的地址,但是其中的某个属性还是在对之前没有改变的数据作引用,这样的性能消耗,就会减少。请看下面动画:
其他的一些技巧:
- 慎用setState,因为容易造成重新渲染。
- 请将方法的bind一律置于constructor。
- 请只传递component需要的props
使用Immutable.js
immutable.js 可以很好的解决问题, 他的基本原则是对于不变的对象返回相同的引用,对于变化的对象,返回新的引用, 因此对于状态的比较只要使用如下代码:
shouldComponentUpdate() { return ref1 !== ref2; } |
参考文章: