有人說 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
對於上面只能采用deepCopy、deepCompare來比較,不僅麻煩而且耗性能。
我們感受一下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,不得不用deepCopy、deepCompare來算出答案,但 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 會被越來越多的項目使用。
