React应用性能优化之IMMUTABLE.JS
前一篇文章记录了React应用的性能优化点,今天还有个更闪耀的 Immutable.js。
Immutable.js
Immutable.js 本身和 React.js 没有必然的联系。它的意义在于它弥补了Javascript没有不可变数据结构的问题。
Javascript中对象都是参考类型,也就是a={a:1}; b=a; b.a=10;你发现a.a也变成10了。可变的好处是节省内存或是利用可变性做一些事情,但是,在复杂的开发中它的副作用远比好处大的多。于是才有了浅copy和深copy,就是为了解决这个问题。举个常见例子:
var defaultConfig = { /* 默认值 */};
var config = $.extend({}, defaultConfig, initConfig); // jQuery用法。initConfig是自定义值
var config = $.extend(true, {}, defaultConfig, initConfig); // 如果对象是多层的,就用到deep-copy了
ES6出现原生的assign方法,但它相当于是浅copy。如果有了不可变的数据结构就省心了,ES5.1中对象有了freeze方法,也是浅copy,a=Object.freeze({a:1}); b=a; b.a=10; a.a还是1。在实际开发中浅copy通常不够。如果用immutableJS:
var defaultConfig = Immutable.fromJS({ /* 默认值 */});
var config = defaultConfig.merge(initConfig); // defaultConfig不会改变,返回新值给config
var config = defaultConfig.mergeDeep(initConfig); // 深层merge
上述用deep-copy也可以做到,差别在于性能。每次deep-copy都要把整个对象递归的复制一份。而Immutable的实现有些像链表,添加一个新结点把旧结点的父子关系转移到新结点上,性能提升很多。ImmutableJS给的远不止这些,它提供了7种不可变的数据结构:List, Stack, Map, OrderedMap, Set, OrderedSet, Record (详见文档Immutable.js,文档很geek,打开console试吧)。immutableJS + 原生Javascript等于真正的函数式编程。
Immutable 优点
1. Immutable 降低了 Mutable 带来的复杂度
可变(Mutable)数据耦合了 Time 和 Value 的概念,造成了数据很难被回溯。
比如下面一段代码:
function touchAndLog(touchFn) {
let data = { key: 'value' };
touchFn(data);
console.log(data.key); // 猜猜会打印什么?
}
在不查看 touchFn 的代码的情况下,因为不确定它对 data 做了什么,你是不可能知道会打印什么(这不是废话吗)。但如果 data 是 Immutable 的呢,你可以很肯定的知道打印的是 value。
2. 节省内存
Immutable.js
使用了 Structure Sharing 会尽量复用内存,甚至以前使用的对象也可以再次被复用。没有被引用的对象会被垃圾回收。
import { Map} from 'immutable';
let a = Map({
select: 'users',
filter: Map({ name: 'Cam' })
})
let b = a.set('select', 'people');
a === b; // false
a.get('filter') === b.get('filter'); // true
上面 a 和 b 共享了没有变化的 filter
节点。
3. Undo/Redo,Copy/Paste,甚至时间旅行这些功能做起来小菜一碟
因为每次数据都是不一样的,只要把这些数据放到一个数组里储存起来,想回退到哪里就拿出对应数据即可,很容易开发出撤销重做这种功能。
为什么在React.js中使用Immutable.js
熟悉React.js的都应该知道,React.js是一个UI = f(states)的框架,为了解决更新的问题,React.js使用了virtual dom,virtual dom通过diff修改dom,来实现高效的dom更新。 听起来很完美吧,但是有一个问题。当state更新时,如果数据没变,你也会去做virtual dom的diff,这就产生了浪费。这种情况其实很常见,如:
熟悉 React 组件生命周期的话都知道:调用 setState 方法总是会触发 render 方法从而进行 vdom re-render 相关逻辑,哪怕实际上你没有更改到 Component.state 。
this.state = {count: 0}
this.setState({count: 0});// 组件 state 并未被改变,但仍会触发 render 方法
为了避免这种性能上的浪费,React 提供了一个 shouldComponentUpdate 来控制触发 vdom re-render 逻辑的条件。于是 PureRenderMixin 作为一种优化技巧被使用。但PureRenderMixin只是简单的浅比较,不使用于多层比较。那怎么办??自己去做复杂比较的话,性能又会非常差。
这时候 immutableJS 就派得上用场了:
var map1 = Immutable.fromJS({a:1, b:1, c:{b:{c:{d:{e:7}}}}});
var map2 = Immutable.fromJS({a:1, b:1, c:{b:{c:{d:{e:7}}}}});
Immutable.is(map1, map2); // true
每一次state更新只要有数据改变,那么PureRenderMixin可以立刻判断出数据改变,可以大大提升性能。
实践
与 React 搭配使用,Pure Render
可以使用 react-immutable-render-mixin 通过es6的修饰器 @decorator 来使用
import React from 'react';
import { immutableRenderDecorator } from 'react-immutable-render-mixin';
@immutableRenderDecorator
class Test extends React.Component {
render() {
return <div></div>;
}
}
或者直接
import immutableRenderMixin from 'react-immutable-render-mixin';
React.createClass({
mixins: [immutableRenderMixin],
render: function() {
return <div className={this.props.className}>foo</div>;
}
});
与 Redux 搭配使用
Redux 是目前流行的 Flux 衍生库。它简化了 Flux 中多个 Store 的概念,只有一个 Store,数据操作通过 Reducer 中实现;同时它提供更简洁和清晰的单向数据流(View -> Action -> Middleware -> Reducer),也更易于开发同构应用。目前已经在我们项目中大规模使用。
由于 Redux 中内置的 combineReducers 和 reducer 中的 initialState 都为原生的 Object 对象,所以不能和 Immutable 原生搭配使用。
幸运的是,Redux 并不排斥使用 Immutable,可以自己重写 combineReducers 或使用 redux-immutablejs 来提供支持。
本文原文链接: facebook immutable.js 意义何在,使用场景? Immutable 详解及 React 中实践基本没有改动。