Shared mutable state is the root of all evil(共享的可變狀態是萬惡之源)前端
-- Pete Huntnode
immutable使用git
項目實踐程序員
有人說 Immutable 能夠給 React 應用帶來數十倍的提高,github
也有人說 Immutable 的引入是近期 JavaScript 中偉大的發明,算法
由於同期 React 太火,它的光芒被掩蓋了。數據庫
這些至少說明 Immutable 是頗有價值的,下面咱們來一探究竟。編程
JavaScript 中的對象通常是可變的(Mutable),由於使用了引用賦值,新的對象簡單的引用了原始對象,改變新的對象將影響到原始對象。Immutable 能夠很好地解決這些問題。redux
Immutable Data 就是一旦建立,就不能再被更改的數據。數組
對 Immutable 對象的 任何修改 或 添加 刪除操做 都會 返回一個新的 Immutable 對象。
Immutable 實現的原理是 Persistent Data Structure(持久化數據結構), 也就是使用舊數據建立新數據時,要保證舊數據同時可用且不變。
同時爲了不 deepCopy 把全部節點都複製一遍帶來的性能損耗,Immutable 使用了 Structural Sharing(結構共享),
即若是對象樹中一個節點發生變化,只修改這個節點和受它影響的父節點,其它節點則進行共享。請看下面動畫:
請移步 http://img.alicdn.com/tps/i2/TB1zzi_KXXXXXctXFXXbrb8OVXX-613-575.gif 觀看
目前流行的 Immutable 庫有兩個:
1. immutable.js
Facebook 工程師 Lee Byron 花費 3 年時間打造,與 React 同期出現,但沒有被默認放到 React 工具集裏(React 提供了簡化的 Helper)。
它內部實現了一套完整的 Persistent Data Structure,還有不少易用的數據類型。
像 `Collection`、`List`、`Map`、`Set`、`Record`、`Seq`。有很是全面的`map`、`filter`、`groupBy`、`reduce``find`函數式操做方法。
同時 API 也儘可能與 Object 或 Array 相似。
其中有 3 種最重要的數據結構說明一下:(Java 程序員應該最熟悉了)
2. seamless-immutable
與 Immutable.js 學院派的風格不一樣,seamless-immutable 並無實現完整的 Persistent Data Structure,
而是使用 `Object.defineProperty`(所以只能在 IE9 及以上使用)擴展了 JavaScript 的 Array 和 Object 對象來實現,
只支持 Array 和 Object 兩種數據類型,API 基於與 Array 和 Object 操持不變。
代碼庫很是小,壓縮後下載只有 2K。而 Immutable.js 壓縮後下載有 16K。
下面上代碼來感覺一下二者的不一樣:
// 原來的寫法 let foo = {a: {b: 1}}; let bar = foo; bar.a.b = 2;
console.log(foo.a.b); // 打印 2 console.log(foo === bar); // 打印 true // 使用 immutable.js 後 import Immutable from 'immutable';
foo = {a: {b: 1}};
foo = Immutable.fromJS (foo);
bar = foo.setIn(['a', 'b'], 2); // 使用 setIn 賦值 改變a.b
console.log(foo.getIn(['a', 'b'])); // 使用 getIn 取值,打印 a.b的值 1
console.log(foo === bar); // 打印 false // 使用 seamless-immutable.js 後
import SImmutable from 'seamless-immutable';
foo = {a: {b: 1}}
foo = SImmutable(foo)
bar = foo.merge({a: { b: 2}}) // 使用 merge 賦值, bar.a.b 爲 2
console.log(foo.a.b); // 像原生 Object 同樣取值, foo.a.b 爲 1 console.log(foo === bar); // 打印 false
1. Immutable 下降了 Mutable 帶來的複雜度 能夠回溯
可變(Mutable)數據耦合了 Time 和 Value 的概念,形成了數據很難被回溯。
好比下面一段代碼:
function touchAndLog(touchFn) { let data = { key: 'value' }; touchFn(data); console.log(data.key); // 猜猜會打印什麼?
//使用immutalbe
data = immutable(data)
tachFn(data)
data.key // value, data數據被固化了, 瞬間複雜度降低 }
在不查看 `touchFn` 的代碼的狀況下,由於不肯定它對 `data` 作了什麼,你是不可能知道會打印什麼(這不是廢話嗎)。
但若是 `data` 是 Immutable 的呢,你能夠很確定的知道打印的是 `value`。
2. 節省內存
Immutable.js 使用了 Structure Sharing 會盡可能複用內存。沒有被引用的對象會被垃圾回收。
import { Map } from 'immutable';
let a = Map({ select: 'users', filter: Map({ name: 'Cam' }) })
let b = a.set('select', 'people'); //? set方法 a === b; // false a.get('filter') === b.get('filter'); // true
上面 a 和 b 共享了沒有變化的 `filter` 節點。
3. Undo/Redo,Copy/Paste, 甚至時間旅行這些功能作起來小菜一碟
由於每次數據都是不同的,只要把這些數據放到一個數組裏儲存起來,
想回退到哪裏就拿出對應數據便可,很容易開發出撤銷重作這種功能。
後面我會提供 Flux 作 Undo 的示例。
4. 併發安全
傳統的併發很是難作,由於要處理各類數據不一致問題,所以『聰明人』發明了各類鎖來解決。
但使用了 Immutable 以後,數據天生是不可變的,併發鎖就不須要了。
然而如今並沒什麼卵用,由於 JavaScript 仍是單線程運行的啊。但將來可能會加入,提早解決將來的問題不也挺好嗎?
5. 擁抱函數式編程
Immutable 自己就是函數式編程中的概念,純函數式編程比面向對象更適用於前端開發。
由於只要輸入一致,輸出必然一致,這樣開發的組件更易於調試和組裝。
像 ClojureScript,Elm 等函數式編程語言中的數據類型天生都是 Immutable 的,
這也是爲何 ClojureScript 基於 React 的框架 --- Om 性能比 React 還要好的緣由。
1. 須要學習新的 API
No Comments
2. 增長了資源文件大小
No Comments
3. 容易與原生對象混淆
這點是咱們使用 Immutable.js 過程當中遇到最大的問題。寫代碼要作思惟上的轉變。
雖然 Immutable.js 儘可能嘗試把 API 設計的原生對象相似,
有的時候仍是很難區別究竟是 Immutable 對象仍是原生對象,容易混淆操做。
Immutable 中的 Map 和 List 雖對應原生 Object 和 Array,
但操做很是不一樣,好比你要用 `map.get('key')` 而不是 `map.key`,`array.get(0)` 而不是 `array[0]`。
另外 Immutable 每次修改都會返回新對象,也很容易忘記賦值。
當使用外部庫的時候,通常須要使用原生對象,也很容易忘記轉換。
下面給出一些辦法來避免相似問題發生:
兩個 immutable 對象可使用 `===` 來比較,這樣是直接比較內存地址,性能最好。
但即便兩個對象的值是同樣的,也會返回 `false`:
let map1 = Immutable.Map({a:1, b:1, c:1});
let map2 = Immutable.Map({a:1, b:1, c:1});
map1 === map2; // false
爲了直接比較對象的值,immutable.js 提供了 `Immutable.is` 來作『值比較』,結果以下:
Immutable.is(map1, map2); // true
`Immutable.is` 比較的是兩個對象的 `hashCode` 或 `valueOf`(對於 JavaScript 對象)。
因爲 immutable 內部使用了 Trie 數據結構來存儲,只要兩個對象的 `hashCode` 相等,值就是同樣的。
這樣的算法避免了深度遍歷比較,性能很是好。
後面會使用 `Immutable.is` 來減小 React 重複渲染,提升性能。
另外,還有 mori、cortex 等,由於相似就再也不介紹。
2. 與 Object.freeze、const 區別
`Object.freeze` 和 ES6 中新加入的 `const` 均可以達到防止對象被篡改的功能, (const的屬性是能夠修改的)
但它們是 shallowCopy 的。對象層級一深就要特殊處理了。
3. Cursor 的概念
這個 Cursor 和 數據庫中的遊標是徹底不一樣的概念。
因爲 Immutable 數據通常嵌套很是深,爲了便於訪問深層數據, Cursor 提供了能夠直接訪問這個深層數據的引用。
import Immutable from 'immutable'; import Cursor from 'immutable/contrib/cursor'; let data = Immutable.fromJS({ a: { b: { c: 1 } } });
// 讓 cursor 指向 { c: 1 } let cursor = Cursor.from(data, ['a', 'b'], newData => {
// 當 cursor 或其子 cursor 執行 update 時調用 console.log(newData);
}); cursor.get('c'); // 1 cursor = cursor.update('c', x => x + 1); cursor.get('c'); // 2
1. 與 React 搭配使用,Pure Render
熟悉 React 的都知道,React 作性能優化時有一個避免重複渲染的大招,就是使用 `shouldComponentUpdate()`,
但它默認返回 `true`,即始終會執行 `render()` 方法,而後作 Virtual DOM 比較,並得出是否須要作真實 DOM 更新,
這裏每每會帶來不少無必要的渲染併成爲性能瓶頸。
固然咱們也能夠在 `shouldComponentUpdate()` 中使用使用 deepCopy 和 deepCompare 來避免無必要的 `render()`,
但 deepCopy 和 deepCompare 通常都是很是耗性能的。
Immutable 則提供了簡潔高效的判斷數據是否變化的方法,只需 `===` 和 `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 (thisProps[key] !== nextProps[key] || !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.addons.PureRenderMixin` 或支持 class 語法的pure-render-decorator來實現。
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); } }
更正
上代碼:
let data = this.state.data; data.times = data.times + 1; this.setState({ data: data }); // 下面打印的結果會是已經加 1 後的值。 由於是同一個引用, 因此不會更新 console.log(this.state.data.times);
由於上面的操做直接改變了this.state.data
的值,因此在shouldComponentUpdate
中nextState.data
和this.state.data
實際上是同一個對象
(2016-12-03更新:這裏容易產生誤區,再說得詳細一點,由於咱們在調用this.setState({ data: data })
時,把原來的state中的state賦給了新的state,
因此下面的nextState.data
和this.state.data
是同一個對象,既然是同一個對象的兩個不一樣引用而已,
那麼不管怎麼比較得出的結果都是nextState.data
和this.state.data
相同,因此返回false
),所以不管怎麼比較都會返回false
,致使組件不更新。
正確的作法應該以下:
let data = _.cloneDeep(this.state.data); data.times = data.times + 1; this.setState({ data: data }); // 上面作了 cloneDeep,下面打印的結果會是已經加 1 後的值。 console.log(this.state.data.times);
這樣的話,沒有改變this.state.data
的值,經過調用setState
,使得在shouldComponentUpdate
中nextState.data
是新的值,能夠與this.state.data
比較,根據比較結果判斷是否更新組件。
使用 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) }) }); }
2. 與 Flux 搭配使用
因爲 Flux 並無限定 Store 中數據的類型,使用 Immutable 很是簡單。
如今是實現一個相似帶有添加和撤銷功能的 Store:
import { Map, OrderedMap } from 'immutable';
let todos = OrderedMap(); let history = []; // 普通數組,存放每次操做後產生的數據 let TodoStore = createStore({ getAll() { return todos; } }); Dispatcher.register(action => { if (action.actionType === 'create') { let id = createGUID(); history.push(todos); // 記錄當前操做前的數據,便於撤銷 todos = todos.set(id, Map({ id: id, complete: false, text: action.text.trim() })); TodoStore.emitChange(); } else if (action.actionType === 'undo') { // 這裏是撤銷功能實現, // 只需從 history 數組中取前一次 todos 便可 if (history.length > 0) { todos = history.pop(); } TodoStore.emitChange(); } });
3. 與 Redux 搭配使用
Redux 是目前流行的 Flux 衍生庫。它簡化了 Flux 中多個 Store 的概念,只有一個 Store,
數據操做經過 Reducer 中實現; 同時它提供更簡潔和清晰的單向數據流(View -> Action -> Middleware -> Reducer),
也更易於開發同構應用。目前已經在咱們項目中大規模使用。
因爲 Redux 中內置的 `combineReducers` 和 reducer 中的 `initialState` 都爲原生的 Object 對象,因此不能和 Immutable 原生搭配使用。
幸運的是,Redux 並不排斥使用 Immutable,能夠本身重寫 `combineReducers` 或使用 redux-immutablejs 來提供支持。
上面咱們提到 Cursor 能夠方便檢索和 update 層級比較深的數據,但由於 Redux 中已經有了 select 來作檢索,Action 來更新數據,所以 Cursor 在這裏就沒有用武之地了。
Immutable 能夠給應用帶來極大的性能提高,可是否使用還要看項目狀況。
因爲侵入性較強,新項目引入比較容易,老項目遷移須要評估遷移。
對於一些提供給外部使用的公共組件,最好不要把 Immutable 對象直接暴露在對外接口中。
若是 JS 原生 Immutable 類型會不會太美,被稱爲 React API 終結者的 Sebastian Markbåge 有一個這樣的提案,可否經過如今還不肯定。
不過能夠確定的是 Immutable 會被愈來愈多的項目使用。
碼這麼多字不容易,喜歡就給個 贊 吧,親