Immutable.js 初識

文章博客地址: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 數據,而 ac 是未變化的數據,因此 map1 和 map2 仍然共享 ac 的數據。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 數據結構的最基礎的抽象類,不能直接構造該類型

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。

參考資料
相關文章
相關標籤/搜索