文章博客地址:http://pinggod.com/2016/Immutable/html
Immutable.js 所建立的數據有一個迷人的特性:數據建立後不會被改變。咱們使用 Immutable.js 的示例來解釋這一特性:react
var Immutable = require('immutable'); var map1 = Immutable.Map({a:1, b:2, c:3}); var map2 = map1.set('b', 50); map1.get('b'); // 2 map2.get('b'); // 50
在上面代碼第三行中,map1 使用 set
方法更新數據,結果返回一個新的 Map 類型數據 map2,map2 包含了更新後的數據,可是 map1 沒有發生變化。這種特性讓咱們在引用數據的時候毫無後顧之憂,由於任何對數據的修改都不會影響最原始的數據。在 Immutable.js 誕生以前,咱們可使用深拷貝的方式模擬這一特性,可是會耗費過多的內存空間和計算力。Immutable.js 相比深拷貝的優點在於區分發生變化的數據和未變化的數據,對於上面的 map1 和 map2,b
是變化的數據,因此 map1 和 map2 各保存一份 b
數據,而 a
和 c
是未變化的數據,因此 map1 和 map2 仍然共享 a
和 c
的數據。git
Immutable Data 鼓勵開發者使用純函數式的開發方式,並從函數式開發中引入了惰性計算的特性。雖然加入了不少函數式的概念,Immutable.js 仍然提供了相似原生 JavaScript Array、Map 和 Set 中的方法,而且提供了在原生 JavasScript 數據和 Immutable 數據之間快速轉換的機制。github
Immutable.js 的 API 主要包含如下幾部分:json
formJS()
,將 JavaScript Object 和 Array 完全轉換爲 Immutable Map 和 List數組
is()
,與 Object.is() 相似都是對值的比較,但它會將 Immutable Iterable 視爲值類型數據而不是引用類型數據,若是兩個 Immutable Iterable 的值相等,則返回 true。與 Object.is()
不一樣的是,is(0, -0) 的結果爲 true緩存
List
,有序索引集,相似於 JavaScript 中的 Array安全
Map
,無序 Iterable,讀寫 Key 的複雜度爲 O(log32 N)
性能優化
OrderedMap
,有序 Map,排序依據是數據的 set() 操做數據結構
Set
,元素爲獨一無二的集合,添加數據和判斷數據是否存在的複雜度爲 O(log32 N)
OrderedSet
,有序 Set,排序依據是數據的 add 操做。
Stack
,有序集合,且使用 unshift(v)
和 shift()
進行添加和刪除操做的複雜度爲 O(1)
Range()
,返回一個 Seq.Indexed 類型的數據集合,該方法接收三個參數 (start = 1, end = infinity, step = 1)
,分別表示起始點、終止點和步長,若是 start 等於 end,則返回空的數據結合
Repeat()
,返回一個 Seq.indexed 類型的數據結合,該方法接收兩個參數 (value,times)
,value 表示重複生成的值,times 表示重複生成的次數,若是沒有指定 times
,則表示生成的 Seq
包含無限個 value
Record
,用於衍生新的 Record 類,進而生成 Record 實例。Record 實例相似於 JavaScript 中的 Object 實例,但只接收特定的字符串做爲 key,且擁有默認值
Seq
,序列(may not be backed by a concrete data structure)
Iterable
,能夠被迭代的 (Key, Value)
鍵值對集合,是 Immutable.js 中其餘全部集合的基類,爲其餘全部集合提供了 基礎的 Iterable 操做函數(好比 map()
和 filter
)
Collection
,建立 Immutable 數據結構的最基礎的抽象類,不能直接構造該類型
Immutable.fromJS({a: {b: [10, 20, 30]}, c: 40}, function (key, value) { var isIndexed = Immutable.Iterable.isIndexed(value); return isIndexed ? value.toList() : value.toOrderedMap(); }); // true, "b", {b: [10, 20, 30]} // false, "a", {a: {b: [10, 20, 30]}, c: 40} // false, "", {"": {a: {b: [10, 20, 30]}, c: 40}}
fromJS() 的使用方式相似於 JSON.parse()
,接收兩個參數:json 數據和 reviver 函數。
List<T>(): List<T> List<T>(iter: Iterable.Indexed<T>): List<T> List<T>(iter: Iterable.Set<T>): List<T> List<K, V>(iter: Iterable.Keyed<K, V>): List<any> List<T>(array: Array<T>): List<T> List<T>(iterator: Iterator<T>): List<T> List<T>(iterable: Object): List<T>
List()
是一個構造方法,能夠用於建立新的 List 數據類型,上面代碼演示了該構造方法接收的參數類型,此外 List 擁有兩個靜態方法:
List.isList(value)
,判斷 value 是不是 List 類型
List.of(...values)
,建立包含 ...values
的列表
下面演示幾個 List 經常使用的操做,更詳細的 API 說明請參考官方文檔:
// 1. 查看 List 長度 const $arr1 = List([1, 2, 3]); $arr1.size // => 3 // 2. 添加或替換 List 實例中的元素 // set(index: number, value: T) // 將 index 位置的元素替換爲 value,即便索引越界也是安全的 const $arr2 = $arr1.set(-1, 0); // => [1, 2, 0] const $arr3 = $arr1.set(4, 0); // => [ 1, 2, 3, undefined, 0 ] // 3. 刪除 List 實例中的元素 // delete(index: number) // 刪除 index 位置的元素 const $arr4 = $arr1.delete(1); // => [ 1, 3 ] // 4. 向 List 插入元素 // insert(index: number, value: T) // 向 index 位置插入 value const $arr5 = $arr1.insert(1, 1.5); // => [ 1, 1.5, 2, 3 ] // 5. 清空 List // clear() const $arr6 = $arr1.clear(); // => []
Map 可使用任何類型的數據做爲 Key 值,並使用 Immutable.is()
方法來比較兩個 Key 值是否相等:
Map().set(List.of(1), 'listofone').get(List.of(1)); // => 'listofone'
可是使用 JavaScript 中的引用類型數據(對象、數組)做爲 Key 值時,雖然有時兩個 Key 很像,但它們也是兩個不一樣的 Key 值:
console.log(Map().set({}, 1).get({})) // => undefined
Map() 是 Map 類型的構造方法,行爲相似於 List(),用於建立新的 Map 實例,此外,還包含兩個靜態方法:Map.isMap() 和 Map.of()。下面介紹幾個 Map 實例的經常使用操做,更詳細的 API 使用說明請參考官方文檔:
// 1. Map 實例的大小 const $map1 = Map({ a: 1 }); $map1.size // => 1 // 2. 添加或替換 Map 實例中的元素 // set(key: K, value: V) const $map2 = $map1.set('a', 2); // => Map { "a": 2 } // 3. 刪除元素 // delete(key: K) const $map3 = $map1.delete('a'); // => Map {} // 4. 清空 Map 實例 const $map4 = $map1.clear(); // => Map {} // 5. 更新 Map 元素 // update(updater: (value: Map<K, V>) => Map<K, V>) // update(key: K, updater: (value: V) => V) // update(key: K, notSetValue: V, updater: (value: V) => V) const $map5 = $map1.update('a', () => (2)) // => Map { "a": 2 } // 6. 合併 Map 實例 const $map6 = Map({ b: 2 }); $map1.merge($map6); // => Map { "a": 1, "b": 2 }
OrderedMap 是 Map 的變體,它除了具備 Map 的特性外,還具備順序性,當開發者遍歷 OrderedMap 的實例時,遍歷順序爲該實例中元素的聲明、添加順序。
Set 和 ES6 中的 Set 相似,都是沒有重複值的集合,OrderedSet 是 Set 的遍歷,能夠保證遍歷的順序性。
// 1. 建立 Set 實例 const $set1 = Set([1, 2, 3]); // => Set { 1, 2, 3 } // 2. 添加元素 const $set2 = $set1.add(1).add(4); // => Set { 1, 2, 3, 4 } // 3. 刪除元素 const $set3 = $set1.delete(3); // => Set { 1, 2 } // 4. 並集 const $set4 = Set([2, 3, 4, 5, 6]); $set1.union($set1); // => Set { 1, 2, 3, 4, 5, 6 } // 5. 交集 $set1.intersect($set4); // => Set { 3, 2 } // 6. 差集 $set1.subtract($set4); // => Set { 1 }
Stack 是基於 Signle-Linked List 實現的可索引集合,使用 unshift(v)
和 shift()
執行添加和刪除元素的複雜度爲 O(1)
。
// 1. 建立 Stack 實例 const $stack1 = Stack([1, 2, 3]); // => Stack [ 1, 2, 3 ] // 2. 取第一個元素 $stack1.peek() // => 1 // 2. 取任意位置元素 $stack1.get(2) // => 3 // 3. 判斷是否存在 $stack1.has(10) // => false
Range(start?, end?, step?) 接收三個可選參數,使用方法以下:
// 1. 不傳參 Range(); // => Range [ 0...Infinity ] // 2. 設置 start 起點 Range(10); // => Range [ 10...Infinity ] // 3. 設置 start 起點和 end 終點 Range(10, 20); // => Range [ 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 ] // 4. 設置 start 起點、end 終點和 step 步長 Range(10, 20, 3); // => Range [ 10, 13, 16, 19 ]
Repeat(value, times?) 接收兩個參數,其中 times
重複次數是可選參數:
Repeat('foo'); // => Repeat [ foo Infinity times ] Repeat('foo', 3); // => Repeat [ foo 3 times ]
相似 Range()
和 Repeat(value)
這樣生成無限長度集合的操做,內部都存在惰性計算的機制,只有真實取值時纔會生成相應的結果。使用 ES6 中的 Generator 函數,能夠輕鬆實現一個惰性計算:
function* bigArr() { for (let i = 0; i < 100000; i++) { console.log(`bigArr(${i}): ${i}`) yield i; } } const arr = bigArr(); for (let i = 0; i < 10; i++) { console.log(arr.next()); } // bigArr(0): 0 // => { value: 0, done: false } // => bigArr(1): 1 // => { value: 1, done: false } // => bigArr(2): 2 // => { value: 2, done: false } // => bigArr(3): 3 // => { value: 3, done: false } // => bigArr(4): 4 // => { value: 4, done: false } // => bigArr(5): 5 // => { value: 5, done: false } // => bigArr(6): 6 // => { value: 6, done: false } // => bigArr(7): 7 // => { value: 7, done: false } // => bigArr(8): 8 // => { value: 8, done: false } // => bigArr(9): 9 // => { value: 9, done: false }
Record 在表現上相似於 ES6 中的 Class,但在某些細節上還有所不一樣。經過 Record() 能夠建立一個新的 Record 類,使用該類能夠建立具體的 Record 實例,該實例包含在 Record() 構造函數中聲明的全部屬性和默認值。若是 Record 實例中的某個屬性被刪除了,則只會講實例中的屬性值恢復爲默認值:
// 1. 建立 Record 實例 const A = Record({ a: 1, b: 2 }); const r = new A({ a: 3 }); // => Record { "a": 3, "b": 2 } // 2. 刪除實例屬性 const rr = r.remove('a'); // => Record { "a": 1, "b": 2 }
此外,Record 實例還具備擴展性:
class ABRecord extends Record({a:1,b:2}) { getAB() { return this.a + this.b; } } var myRecord = new ABRecord({b: 3}) myRecord.getAB() // => 4
Seq 有兩個特色:immutable
,一旦建立就不能被修改;lazy
,惰性求值。在下面的代碼中,雖然組合了多種遍歷操做,但實際上並不會有任何的求值操做,只是純粹的聲明一個 Seq:
var oddSquares = Immutable.Seq.of(1,2,3,4,5,6,7,8) .filter(x => x % 2) .map(x => x * x);
若是要從 oddSquares 中取出索引爲 1 的元素,則執行過程爲:
console.log(oddSquares.get(1)); // filter(1) // filter(2) // filter(3) // map(3) // => 9
Seq() 是 Seq 的構造方法,它根據傳入的參數類型,輸出響應的 Seq 類型:
輸入 Seq,輸出 Seq
輸入 Iterable,輸出同類型的 Seq(Keyed, Indexed, Set)
輸入 Array-like,輸出 Seq.Indexed
輸入附加 Iterator 的 Object,輸出 Seq.Indexed
輸入 Iterator,輸出 Seq。indexed
輸入 Object,輸出 Seq.Keyed
默認狀況下 Seq 的惰性計算結果不會被緩存,好比在下面的代碼中,因爲每一個 join()
都會遍歷執行 map,因此 map 總共執行了六次:
var squares = Seq.of(1,2,3).map(x => x * x); squares.join() + squares.join();
若是開發者知道 Seq
的結果會被反覆用到,那麼就可使用 cacheResult()
將惰性計算的結果保存到內存中:
var squares = Seq.of(1,2,3).map(x => x * x).cacheResult(); squares.join() + squares.join();
Iterable 是鍵值對形式的集合,其實例能夠執行遍歷操做,是 immutable.js 中其餘數據類型的基類,全部擴展自 Iterable 的數據類型均可以使用 Iterable 所聲明的方法,好比 map 和 filter 等。
Collection 是 Concrete Data Structure 的基類,使用該類時須要至少繼承其子類中的一個:Collection.Keyed / Collection.Indexed / Collection.Set。
在 React 官方文檔的《Advanced Performance》 一節中,專門對 React 的性能瓶頸、優化方式作了詳細的解析。當一個 React 組件的 props 和 state 發生變化時,React 會根據變化後的 props 和 state 建立一個新的 virtual DOM,而後比較新舊兩個 vritual DOM 是否一致,只有當二者不一樣時,React 纔會將 virtual DOM 渲染真實的 DOM 結點,而對 React 進行性能優化的核心就是減小渲染真實 DOM 結點的頻率,間接地指出開發者應該準確判斷 props 和 state 是否真正發生了變化。
在比對新舊 vritual DOM 和渲染真實 DOM 前,React 爲開發者提供了 shouldComponentUpdate()
方法中斷接下來的比對和渲染操做,默認狀況下,該方法總會返回 true
,若是它返回 false
,則不執行比對和渲染操做:
// 最簡單的實現: shouldComponentUpdate (nextProps) { return this.props.value !== nextProps.value; }
看起來挺簡單,實在否則。當咱們須要比對的值是對象、數組等引用值時,就會出現問題:
// 假設 this.props.value 是 { foo: 'bar' } // 假設 nextProps.value 是 { foo: 'bar' }, // 顯然這二者引用的內存地址不一樣,但它們具備相同的值,這種時候不該該繼續執行渲染 this.props.value !== nextProps.value; // true
若是數據是 Immutable Data 的話,那麼數據發生變化就會生成新的對象,開發者只須要檢查對象應用是否發生變化便可:
var SomeRecord = Immutable.Record({ foo: null }); var x = new SomeRecord({ foo: 'bar' }); var y = x.set('foo', 'baz'); x === y; // false
處理這一問題的另外一種方式是經過 setter 設置 flag 對髒數據進行檢查,但冗雜的代碼是在讓人頭疼。
Immutable.js 所提供的 Immutable Data 和 JavaScript 固有的 Mutable Data 各有優點,將來 ECAMScript 有可能制定一套原生的 Immutable Data 規範,在這以前,Immutable.js 是一個不錯的選擇。以前已經寫文章熟悉過 Lodash 這一工具庫,Immutable 內部也封裝了諸多經常使用的數據操做函數,因此若是讓我來選擇的話,在 React 技術棧中我會更偏心 Immutable。