React应用性能优化之IMMUTABLE.JS

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.js

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 中实践基本没有改动。

0%