有人說 Immutable 能夠給 React 應用帶來數十倍的提高,也有人說 Immutable 的引入是近期 JavaScript 中偉大的發明,由於同期 React 太火,它的光芒被掩蓋了。這些至少說明 Immutable 是頗有價值的,下面咱們來一探究竟。前端
Immutable是一旦建立,就不能被更改的數據。對Immutable對象的任何修改或添加刪除操做都會返回一個新的Immutable對象。Immutable實現的原理是Persistent Data Structure(持久化數據結構),也就是是永久數據建立新數據時,要保證舊數據同時可用且不變。同時爲了不deepCopy把全部節點都複製一遍帶來的性能損耗,Immutable使用了Structural Sharing(結構共享),即若是對象樹結點發生變化,只修改這個結點和受它影響的父節點,其餘結點進行共享。react
初識:git
讓咱們看下面一段代碼:github
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
雖然這樣能夠節約內存,但當應用複雜後,形成了狀態不可控,是很大的隱患,節約內存的優勢變得得不償失。redux
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 還要好的緣由。
熟悉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 變化後,不會再渲染樹中的全部節點,而是隻渲染圖中綠色的部分:
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) }) }); }
目標:將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);
Immutable 能夠給應用帶來極大的性能提高,可是否使用還要看項目狀況。因爲侵入性較強,新項目引入比較容易,老項目遷移須要評估遷移。對於一些提供給外部使用的公共組件,最好不要把 Immutable 對象直接暴露在對外接口中。
若是 JS 原生 Immutable 類型會不會太美,被稱爲 React API 終結者的 Sebastian Markbåge 有一個這樣的提案,可否經過如今還不肯定。不過能夠確定的是 Immutable 會被愈來愈多的項目使用。