Immutable.js 所建立的資料有一個迷人的特性:資料建立後不會被改變。我們使用 Immutable.js 的示例來解釋這一特性:
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
的資料。
概覽
Immutable Data 鼓勵開發者使用純函式式的開發方式,並從函式式開發中引入了惰性計算的特性。雖然加入了很多函式式的概念,Immutable.js 仍然提供了類似原生 JavaScript Array、Map 和 Set 中的方法,並且提供了在原生 JavasScript 資料和 Immutable 資料之間快速轉換的機制。
Immutable.js 的 API 主要包含以下幾部分:
-
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 資料結構的最基礎的抽象類,不能直接構造該型別
1. fromJS()
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 函式。
2. List
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();
// => []
3. Map
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 的例項時,遍歷順序為該例項中元素的宣告、新增順序。
4. Set
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 }
5. Stack
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
6. Range() 和 Repeat()
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 }
7. Record
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
8. Seq
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();
9. Iterable 和 Collection
Iterable 是鍵值對形式的集合,其例項可以執行遍歷操作,是 immutable.js 中其他資料型別的基類,所有擴充套件自 Iterable 的資料型別都可以使用 Iterable 所宣告的方法,比如 map 和 filter 等。
Collection 是 Concrete Data Structure 的基類,使用該類時需要至少繼承其子類中的一個:Collection.Keyed / Collection.Indexed / Collection.Set。
React
在 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。