有人說 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 會被越來越多的項目使用。