Immutable.js及在React中的應用
來自: 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 已經實現,此處無法發揮作用。
參考
- http://非死book.github.io/immutable-js/
- http://rhadow.github.io/2015/05/10/flux-immutable/
- http://boke.io/immutable-js/
- https://en.wikipedia.org/wiki/Persistent_data_structure
- http://blog.nextoffer.com/why-we-invest-in-tools/
- https://github.com/camsong/blog/issues/3
- http://jlongster.com/Using-Immutable-Data-Structures-in-JavaScript
</ul> </div>