Immutable.js及在React中的應用

yy123321 8年前發布 | 30K 次閱讀 React JavaScript開發 JavaScript

來自: http://zhenhua-lee.github.io/css/Immutable.html

1. 為什么需要Immutable.js

1.1 引用帶來的副作用

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

</div>

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

業務場景1:

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

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

業務場景2:

var obj = {
 count: 1
};

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

1.2 深度拷貝的性能問題

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

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 的拷貝明顯是多余,因此深度拷貝存在性能缺陷的問題。

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

2. Immutable的優點

2.1 Persistent data structure

Immutable.js提供了7種不可變的數據類型: List 、 Map 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 請查看 wikipedia

2.2 structural sharing

當我們對一個Immutable對象進行操作的時候,ImmutableJS會只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對象(例如 List 、 Map )可以通過 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的類似,降低學習成本,有些方法提供了便捷操作,例如 setIn 、 UpdateIn 可以進行深度操作。

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 , PureRenderMixin 對 shouldComponentUpdate 方法進行了覆蓋,但是 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進行操作的時候非常方便。

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

  • 源文件過大: 源碼總共有5k多行,壓縮后有16kb
  • 類型轉換: 如果需要頻繁地與服務器交互,那么Immutable對象就需要不斷地與原生js進行轉換,操作起來顯得很繁瑣
  • 侵入性: 例如引用第三方組件的時候,就不得不進行類型轉換;在使用react-redux時,connect的 shouldComponentUpdate 已經實現,此處無法發揮作用。

參考

 本文由用戶 yy123321 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!