在react/redux中使用Immutable

有人说 Immutable 可以给 React 应用带来数十倍的提升,也有人说 Immutable 的引入是近期 JavaScript 中伟大的发明,因为同期 React 太火,它的光芒被掩盖了。这些至少说明 Immutable 是很有价值的,下面我们来一探究竟。

1、什么是Immutable?

Immutable是一旦创建,就不能被更改的数据。对Immutable对象的任何修改或添加删除操作都会返回一个新的Immutable对象。Immutable实现的原理是Persistent Data Structure(持久化数据结构),也就是是永久数据创建新数据时,要保证旧数据同时可用且不变。同时为了避免deepCopy把所有节点都复制一遍带来的性能损耗,Immutable使用了Structural Sharing(结构共享),即如果对象树结点发生变化,只修改这个结点和受它影响的父节点,其他结点进行共享。

 

 

初识:

让我们看下面一段代码:

function keyLog(touchFn) {
  let data = { key: 'value' };
  fun(data);
  console.log(data.key); // 猜猜会打印什么?
}

不查看fun方法,不知道它对data做了什么,无法确认会打印什么。但如果data是Immutable,你可以确定打印的就是value:

function keyLog(touchFn) {
  let data = Immutable.Map({ key: 'value' });
  fun(data);
  console.log(data.get('key'));  // value
}

JavaScript中的Object与Array等使用的是引用赋值,如果新的对象简单的引用了原始对象,改变新的对象也将影响旧的。

foo = {a:1};  bar = foo;  bar.a = 2;
foo.a // 2

虽然这样可以节约内存,但当应用复杂后,造成了状态不可控,是很大的隐患,节约内存的优点变得得不偿失。

Immutable则不一样,相应的:

foo = Immutable.Map({ a: 1});
bar = foo.set('a', 2);
foo.get('a') // 1

 

 简洁:

在redux中,它的最优做法是每个reducer都返回一个新的对象(数组),所以我们常常会看到这样的代码:

// reducer
...
return [
   ...oldArr.slice(0,3),
   newValue,
   ...oldArr.slice(4)   
];

为了返回新的对象(数组),不得不有上面奇怪的样子,而在使用更深的数据结构时会变的更棘手。
让我们看看Immutable的做法:

// reducer
...
return oldArr.set(4, newValue);

是不是很简洁?

 

关于"===":

众所周知,对于Object与Array的===比较,是对引用地址的比较,而不是“值比较”,如:

{a:1, b:2, c:3} === {a:1, b:2, c:3}; // false
[1, 2, [3, 4]] === [1, 2, [3, 4]]; // false

对于上面只能采用deepCopydeepCompare来比较,不仅麻烦而且耗性能。

我们感受一下Immutbale的做法:

map1 = Immutable.Map({a:1, b:2, c:3});
map2 = Immutable.Map({a:1, b:2, c:3});
Immutable.is(map1, map2); // true 比较值
map1 === map2; // false 比较地址
// List1 = Immutable.List([1, 2, Immutable.List[3, 4]]);
List1 = Immutable.fromJS([1, 2, [3, 4]]);
List2 = Immutable.fromJS([1, 2, [3, 4]]);
Immutable.is(List1, List2); // true

似乎有阵清风吹过。

Immutable.is 比较的是两个对象的 hashCode 或 valueOf(对于 JavaScript 对象)。由于 immutable 内部使用了 Trie 数据结构来存储,只要两个对象的 hashCode 相等,值就是一样的。这样的算法避免了深度遍历比较,性能非常好。

Immutable使用了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 节点。

 

并发安全:

传统的并发非常难做,因为要处理各种数据不一致问题,因此『聪明人』发明了各种锁来解决。但使用了 Immutable 之后,数据天生是不可变的,并发锁就不需要了

然而现在并没什么卵用,因为 JavaScript 还是单线程运行的啊。但未来可能会加入,提前解决未来的问题不也挺好吗?

函数式编程:

Immutable本身就是函数式编程中的概念,纯函数式编程比面向对象更适用于前端开发。因为只要输入一致,输出必然一致,这样开发的组件更易于调试和组装。

像 ClojureScript,Elm 等函数式编程语言中的数据类型天生都是 Immutable 的,这也是为什么 ClojureScript 基于 React 的框架 --- Om 性能比 React 还要好的原因。

 

2、在react中使用Immutable

熟悉React的都知道,React做性能优化时有个大招,就是使用shouldComponentUpdate(),但它默认返回true,即始终会执行render()方法,后面做Virtual DOM比较,并得出是都需要做真是DOM更新,这里往往会带来很多务必要的渲染成为性能瓶颈。

在使用原生属性时,为了得出shouldComponetUpdate正确的true or false,不得不用deepCopydeepCompare来算出答案,但 deepCopy 和 deepCompare 一般都是非常耗性能的

而在有了Immutable之后,Immutable 则提供了简洁高效的判断数据是否变化的方法,来减少 React 重复渲染,提高性能,只需 === 和 is 比较就能知道是否需要执行 render(),而这个操作几乎 0 成本,所以可以极大提高性能。修改后的 shouldComponentUpdate 是这样的:

import { is } from 'immutable';

shouldComponentUpdate: (nextProps = {}, nextState = {}) => {
  const thisProps = this.props || {}, thisState = this.state || {};

  if (Object.keys(thisProps).length !== Object.keys(nextProps).length ||
      Object.keys(thisState).length !== Object.keys(nextState).length) {
    return true;
  }

  for (const key in nextProps) {
    if (!is(thisProps[key], nextProps[key])) {
      return true;
    }
  }

  for (const key in nextState) {
    if (thisState[key] !== nextState[key] || !is(thisState[key], nextState[key])) {
      return true;
    }
  }
  return false;
}

使用 Immutable 后,如下图,当红色节点的 state 变化后,不会再渲染树中的所有节点,而是只渲染图中绿色的部分:

setState 的一个技巧

React 建议把 this.state 当作 Immutable 的,因为修改前需要做一个 deepCopy,显得麻烦:

 

import '_' from 'lodash';

const Component = React.createClass({
  getInitialState() {
    return {
      data: { times: 0 }
    }
  },
  handleAdd() {
    let data = _.cloneDeep(this.state.data);
    data.times = data.times + 1;
    this.setState({ data: data });
    // 如果上面不做 cloneDeep,下面打印的结果会是已经加 1 后的值。
    console.log(this.state.data.times);
  }
}

使用 Immutable 后:

  getInitialState() {
    return {
      data: Map({ times: 0 })
    }
  },
  handleAdd() {
    this.setState({ data: this.state.data.update('times', v => v + 1) });
    // 这时的 times 并不会改变
    console.log(this.state.data.get('times'));
  }

上面的 handleAdd 可以简写成:

  handleAdd() {
    this.setState(({data}) => ({
      data: data.update('times', v => v + 1) })
    });
  }

 

3、如何在Redux中使用Immutable

目标:将state -> Immutable化。
关键的库:gajus/redux-immutable
将原来 Redux提供的combineReducers改由上面的库提供: 

 

// rootReduers.js
// import { combineReducers } from 'redux'; // 旧的方法
import { combineReducers } from 'redux-immutable'; // 新的方法

import prop1 from './prop1';
import prop2 from './prop2';
import prop3 from './prop3';

const rootReducer = combineReducers({
  prop1, prop2, prop3,
});


// store.js
// 创建store的方法和常规一样
import { createStore } from 'redux';
import rootReducer from './reducers';

const store = createStore(rootReducer);
export default store;

通过新的combineReducers将把store对象转化成Immutable,在container中使用时也会略有不同(但这正是我们想要的):

const mapStateToProps = (state) => ({
  prop1: state.get('prop1'),
  prop2: state.get('prop2'),
  prop3: state.get('prop3'),
  next: state.get('next'),
});
export default connect(mapStateToProps)(App);

4、总结

Immutable 可以给应用带来极大的性能提升,但是否使用还要看项目情况。由于侵入性较强,新项目引入比较容易,老项目迁移需要评估迁移。对于一些提供给外部使用的公共组件,最好不要把 Immutable 对象直接暴露在对外接口中。

如果 JS 原生 Immutable 类型会不会太美,被称为 React API 终结者的 Sebastian Markbåge 有一个这样的提案,能否通过现在还不确定。不过可以肯定的是 Immutable 会被越来越多的项目使用。

 

posted @ 2018-02-25 13:42  张三的美丽家园  阅读(2203)  评论(0编辑  收藏  举报