Immutable.js及在React中的应用

  React       2016-07-01

1. 为什么需要Immutable.js

1.1 引用带来的副作用

Shared mutable state is the root of all evil(共享的可变状态是万恶之源)

JavaScript(ES5)中存在两类数据结构: primitive value(string、number、boolean、null、undefined)、object(reference)。在编译型语言(例如java)也存在object,但是JS中的对象非常灵活、多变,这给我们的开发带来了不少好处,但是也引起了非常多的问题。

业务场景1:

  1. var obj = {
  2. count: 1
  3. };
  4. var clone = obj;
  5. clone.count = 2;
  6. console.log(clone.count) // 2
  7. console.log(obj.count) // 2

业务场景2:

  1. var obj = {
  2. count: 1
  3. };
  4. unKnownFunction(obj);
  5. console.log(obj.count) // 不知道结果是多少?

1.2 深度拷贝的性能问题

针对引用的副作用,有人会提出可以进行深度拷贝(deep clone), 请看下面深度拷贝的代码:

  1. function isObject(obj) {
  2. return typeof obj === 'object';
  3. }
  4. function isArray(arr) {
  5. return Array.isArray(arr);
  6. }
  7. function deepClone(obj) {
  8. if (!isObject(obj)) return obj;
  9. var cloneObj = isArray(obj) ? [] : {};
  10. for(var key in obj) {
  11. if (obj.hasOwnProperty(key)) {
  12. var value = obj[key];
  13. var copy = value;
  14. if (isObject(value)) {
  15. cloneObj[key] = deepClone(value);
  16. } else {
  17. cloneObj[key] = value;
  18. }
  19. }
  20. }
  21. return cloneObj;
  22. }
  23. var obj = {
  24. age: 5,
  25. list: [1, 2, 3]
  26. };
  27. var obj2 = deepClone(obj)
  28. console.log(obj.list === obj2.list) // false

假如仅仅只是对obj.age进行操作,使用深度拷贝同样需要拷贝list字段,而两个对象的list值是相同的,对list的拷贝明显是多余,因此深度拷贝存在性能缺陷的问题。

  1. var obj = {
  2. age: 5,
  3. list: [1, 2, 3]
  4. };
  5. var obj2 = deepClone(obj)
  6. obj2.age = 6;
  7. // 假如仅仅只对age字段操作,使用深度拷贝(deepClone函数)也对list进行了复制,
  8. // 这样明显是多余的,存在性能缺陷

1.3 JS本身的无力

在JS中实现数据不可变,有两个方法: const(ES6)、Object.freeze(ES5)。但是这两种方法都是shallow处理,遇到嵌套多深的结构就需要递归处理,又会存在性能上的问题。

2. Immutable的优点

2.1 Persistent data structure

Immutable.js提供了7种不可变的数据类型: ListMapStackOrderedMapSetOrderedSetRecord。对Immutable对象的操作均会返回新的对象,例如:

  1. var obj = {count: 1};
  2. var map = Immutable.fromJS(obj);
  3. var map2 = map.set('count', 2);
  4. console.log(map.get('count')); // 1
  5. console.log(map2.get('count')); // 2

关于Persistent data structure 请查看 wikipedia

2.2 structural sharing

当我们对一个Immutable对象进行操作的时候,ImmutableJS基于哈希映射树(hash map tries)和vector map tries,只clone该节点以及它的祖先节点,其他保持不变,这样可以共享相同的部分,大大提高性能。

  1. var obj = {
  2. count: 1,
  3. list: [1, 2, 3, 4, 5]
  4. }
  5. var map1 = Immutable.fromJS(obj);
  6. var map2 = map1.set('count', 2);
  7. console.log(map1.list === map2.list); // true

从网上找一个图片来说明结构共享的过程:

Immutable

2.3 support lazy operation

ImmutableJS借鉴了Clojure、Scala、Haskell这些函数式编程语言,引入了一个特殊结构Seq(全称Sequence), 其他Immutable对象(例如ListMap)可以通过toSeq进行转换。

Seq具有两个特征: 数据不可变(Immutable)、计算延迟性(Lazy)。在下面的demo中,直接操作1到无穷的数,会超出内存限制,抛出异常,但是仅仅读取其中两个值就不存在问题,因为没有对map的结果进行暂存,只是根据需要进行计算。

  1. Immutable.Range(1, Infinity)
  2. .map(n => -n)
  3. // Error: Cannot perform this action with an infinite size.
  4. Immutable.Range(1, Infinity)
  5. .map(n => -n)
  6. .take(2)
  7. .reduce((r, n) => r + n, 0);
  8. // -3

2.4 强大的API机制

ImmutableJS的文档很Geek,提供了大量的方法,有些方法沿用原生js的类似,降低学习成本,有些方法提供了便捷操作,例如setIn、UpdateIn可以进行深度操作。

  1. var obj = {
  2. a: {
  3. b: {
  4. list: [1, 2, 3]
  5. }
  6. }
  7. };
  8. var map = Immutable.fromJS(obj);
  9. var map2 = Immutable.updateIn(['a', 'b', 'list'], (list) => {
  10. return list.push(4);
  11. });
  12. console.log(map2.getIn(['a', 'b', 'list']))
  13. // List [ 1, 2, 3, 4 ]

3. 在React中的实践

3.1 快 - 性能优化

React是一个UI = f(state)库,为了解决性能问题引入了virtual dom,virtual dom通过diff算法修改DOM,实现高效的DOM更新。

听起来很完美吧,但是有一个问题: 当执行setState时,即使state数据没发生改变,也会去做virtual dom的diff,因为在React的声明周期中,默认情况下shouldComponentUpdate总是返回true。那如何在shouldComponentUpdate进行state比较?

React的解决方法: 提供了一个PureRenderMixin, PureRenderMixinshouldComponentUpdate方法进行了覆盖,但是PureRenderMixin里面是浅比较:

  1. var ReactComponentWithPureRenderMixin = {
  2. shouldComponentUpdate: function(nextProps, nextState) {
  3. return shallowCompare(this, nextProps, nextState);
  4. },
  5. };
  6. function shallowCompare(instance, nextProps, nextState) {
  7. return (
  8. !shallowEqual(instance.props, nextProps) ||
  9. !shallowEqual(instance.state, nextState)
  10. );
  11. }

浅比较只能进行简单比较,如果数据结构复杂的话,依然会存在多余的diff过程,说明PureRenderMixin依然不是理想的解决方案。

Immutable来解决: 因为Immutable的结构不可变性&&结构共享性,能够快速进行数据的比较:

  1. shouldComponentUpdate: function(nextProps, nextState) {
  2. return deepCompare(this, nextProps, nextState);
  3. },
  4. function deepCompare(instance, nextProps, nextState) {
  5. return !Immutable.is(instance.props, nextProps) ||
  6. !Immutable.is(instance.state, nextState);
  7. }

3.2 安全 - 保证state操作的安全

当我们在React中执行setState的时候,需要注意的,state merge过程是shallow merge:

  1. getInitState: function () {
  2. return {
  3. count: 1,
  4. user: {
  5. school: {
  6. address: 'beijing',
  7. level: 'middleSchool'
  8. }
  9. }
  10. }
  11. },
  12. handleChangeSchool: function () {
  13. this.setState({
  14. user: {
  15. school: {
  16. address: 'shanghai'
  17. }
  18. }
  19. })
  20. }
  21. render() {
  22. console.log(this.state.user.school);
  23. // {address: 'shanghai'}
  24. }

为了让大家安心,贴上React中关于state merge的源码:

  1. // 在 ReactCompositeComponent.js中完成state的merge,其中merger的方法来源于
  2. // `Object.assign`这个模块
  3. function assign(target, sources) {
  4. ....
  5. var to = Object(target);
  6. ...
  7. for (var nextIndex = 1; nextIndex < arguments.length; nextIndex++) {
  8. var nextSource = arguments[nextIndex];
  9. var from = Object(nextSource);
  10. ...
  11. for (var key in from) {
  12. if (hasOwnProperty.call(from, key)) {
  13. to[key] = from[key];
  14. }
  15. }
  16. }
  17. return to
  18. }

3.3 方便 - 强大的API

ImmutableJS里面拥有强大的API,并且文档写的很Geek,在对state、store进行操作的时候非常方便。

  1. var obj = {
  2. name: 'mt',
  3. info: {
  4. address: 'bj'
  5. }
  6. };
  7. Object.freeze(obj);
  8. obj.name = 'mt&&dp';
  9. obj.info.address = 'bj&&sh';
  10. console.log(obj.name); // 'mt'(no change)
  11. console.log(obj.info.address); // 'bj&&sh'(change)

3.4 历史 - 实现回退

可以保存state的每一个状态,并保证该状态不会被修改,这样就可以实现历史记录的回退。

4. React中引入Immutable.js带来的问题

  • 源文件过大: 源码总共有5k多行,压缩后有16kb
  • 类型转换: 如果需要频繁地与服务器交互,那么Immutable对象就需要不断地与原生js进行转换,操作起来显得很繁琐
  • 侵入性: 例如引用第三方组件的时候,就不得不进行类型转换;在使用react-redux时,connect的shouldComponentUpdate已经实现,此处无法发挥作用。

参考:

本文最后更新于2017-01-08 00:46:54