Immutable.js及在React中的應用

1. 爲何須要Immutable.js

1.1 引用帶來的反作用

Shared mutable state is the root of all evil(共享的可變狀態是萬惡之源)javascript

javascript(es5)中存在兩類數據結構: primitive value(string、number、boolean、null、undefined)、object(reference)。在編譯型語言(例如java)也存在object,可是js中的對象很是靈活、多變,這給咱們的開發帶來了很多好處,可是也引發了很是多的問題。java

業務場景1:react

var obj = {
  count: 1
};
var clone = obj;
clone.count = 2;

console.log(clone.count) // 2
console.log(obj.count) // 2

業務場景2:git

var obj = {
 count: 1
};

unKnownFunction(obj);
console.log(obj.count) // 不知道結果是多少?

1.2 深度拷貝的性能問題

針對引用的反作用,有人會提出能夠進行深度拷貝(deep clone), 請看下面深度拷貝的代碼:es6

function isObject(obj) {
  return typeof obj === 'object';
}

function isArray(arr) {
  return Array.isArray(arr);
}
function deepClone(obj) {
  if (!isObject(obj))  return obj;
  var cloneObj = isArray(obj) ? [] : {};
  
  for(var key in obj) {
    if (obj.hasOwnProperty(key)) {
      var value = obj[key];
      var copy = value;
      
      if (isObject(value)) {
        cloneObj[key] = deepClone(value);
      } else {
        cloneObj[key] = value;
      }
    }
  }
  return cloneObj;
}

var obj = {
  age: 5,
  list: [1, 2, 3]
};

var obj2 = deepClone(obj)
console.log(obj.list === obj2.list) // false

假如僅僅只是對obj.age進行操做,使用深度拷貝一樣須要拷貝list字段,而兩個對象的list值是相同的,對list的拷貝明顯是多餘,所以深度拷貝存在性能缺陷的問題。github

var obj = {
  age: 5,
  list: [1, 2, 3]
};
var obj2 = deepClone(obj)
obj2.age = 6;
// 假如僅僅只對age字段操做,使用深度拷貝(deepClone函數)也對list進行了複製,
// 這樣明顯是多餘的,存在性能缺陷

1.3 js自己的無力

在js中實現數據不可變,有兩個方法: const(es6)、Object.freeze(es5)。可是這兩種方法都是shallow處理,遇到嵌套多深的結構就須要遞歸處理,又會存在性能上的問題。算法

2. Immutable的優勢

2.1 Persistent data structure

Immutable.js提供了7種不可變的數據類型: ListMap Stack OrderedMap Set OrderedSet Record。對Immutable對象的操做均會返回新的對象,例如:編程

var obj = {count: 1};
var map = Immutable.fromJS(obj);
var map2 = map.set('count', 2);

console.log(map.get('count')); // 1
console.log(map2.get('count')); // 2

關於Persistent data structure 請查看 wikipediaredux

2.2 structural sharing

當咱們對一個Immutable對象進行操做的時候,ImmutableJS基於哈希映射樹(hash map tries)和vector map tries,只clone該節點以及它的祖先節點,其餘保持不變,這樣能夠共享相同的部分,大大提升性能。安全

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

console.log(Immutable.is(map1.list, map2.list)); // true

從網上找一個圖片來講明結構共享的過程:

圖片描述

2.3 support lazy operation

ImmutableJS借鑑了Clojure、Scala、Haskell這些函數式編程語言,引入了一個特殊結構Seq(全稱Sequence), 其餘Immutable對象(例如ListMap)能夠經過toSeq進行轉換。

Seq具備兩個特徵: 數據不可變(Immutable)、計算延遲性(Lazy)。在下面的demo中,直接操做1到無窮的數,會超出內存限制,拋出異常,可是僅僅讀取其中兩個值就不存在問題,由於沒有對map的結果進行暫存,只是根據須要進行計算。

Immutable.Range(1, Infinity)
.map(n => -n)
// Error: Cannot perform this action with an infinite size.

Immutable.Range(1, Infinity)
.map(n => -n)
.take(2)
.reduce((r, n) => r + n, 0); 
// -3

2.4 強大的API機制

ImmutableJS的文檔很Geek,提供了大量的方法,有些方法沿用原生js的相似,下降學習成本,有些方法提供了便捷操做,例如setInUpdateIn能夠進行深度操做。

var obj = {
  a: {
    b: {
      list: [1, 2, 3]
    }
  }
};
var map = Immutable.fromJS(obj);
var map2 = Immutable.updateIn(['a', 'b', 'list'], (list) => {
  return list.push(4);
});

console.log(map2.getIn(['a', 'b', 'list']))
// 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裏面是淺比較:

var ReactComponentWithPureRenderMixin = {
  shouldComponentUpdate: function(nextProps, nextState) {
    return shallowCompare(this, nextProps, nextState);
  },
};

function shallowCompare(instance, nextProps, nextState) {
  return (
    !shallowEqual(instance.props, nextProps) ||
    !shallowEqual(instance.state, nextState)
  );
}

淺比較只能進行簡單比較,若是數據結構複雜的話,依然會存在多餘的diff過程,說明PureRenderMixin依然不是理想的解決方案。

Immutable來解決: 由於Immutable的結構不可變性&&結構共享性,可以快速進行數據的比較:

shouldComponentUpdate: function(nextProps, nextState) {
  return deepCompare(this, nextProps, nextState);
},
  
function deepCompare(instance, nextProps, nextState) {
    return !Immutable.is(instance.props, nextProps) || 
        !Immutable.is(instance.state, nextState);
}

3.2 安全 - state操做的安全

當咱們在React中執行setState的時候,須要注意的,state merge過程是shallow merge:

getInitState: function () {
  return {
    count: 1,
    user: {
      school: {
        address: 'beijing',
        level: 'middleSchool'
      }
    }
  }
},
handleChangeSchool: function () {
  this.setState({
    user: {
      school: {
        address: 'shanghai'
      }
    }
  })
}
render() {
  console.log(this.state.user.school);
  // {address: 'shanghai'}
}

爲了讓你們安心,貼上React中關於state merge的源碼:

// 在 ReactCompositeComponent.js中完成state的merge,其中merger的方法來源於
// `Object.assign`這個模塊
function assign(target, sources) {
  ....
  var to = Object(target);
  ... 
  for (var nextIndex = 1; nextIndex < arguments.length; nextIndex++) {
    var nextSource = arguments[nextIndex];
    var from = Object(nextSource);
    ...
    for (var key in from) {
      if (hasOwnProperty.call(from, key)) {
        to[key] = from[key];
      }
    }
  }
  return to
}

3.3 方便 - 強大的API

ImmutableJS裏面擁有強大的API,而且文檔寫的很Geek,在對state、store進行操做的時候很是方便。

3.4 歷史 - 實現回退

能夠保存state的每個狀態,並保證該狀態不會被修改,這樣就能夠實現歷史記錄的回退。

4. React中引入Immutable.js帶來的問題

  • 源文件過大: 源碼總共有5k多行,壓縮後有16kb

  • 類型轉換: 若是須要頻繁地與服務器交互,那麼Immutable對象就須要不斷地與原生js進行轉換,操做起來顯得很繁瑣

  • 侵入性: 例如引用第三方組件的時候,就不得不進行類型轉換;在使用react-redux時,connect的shouldComponentUpdate已經實現,此處沒法發揮做用。

參考

相關文章
相關標籤/搜索