標籤:

可變對象與 immutable.js

大家開發中,應該都會遇到這種情況,對象被賦值幾次後,更改了對象的某個屬性,發現其被引用的對象也發生了變化。這就是可變對象。

可變(Mutable)對象

先舉個栗子,請看下面的可變對象

const foo = { a: 1};const bar = foo;bar.a = 2; // foo.a 也變為 2

當 bar.a 做了賦值之後,foo 對象的相應屬性。這種靈活性帶來的問題是:除非你清楚的知道要修改的 對象/屬性 及其關聯的對象,否則很難發現它是何時被變化的。

JavaScript 中,Object 和 Array 是可變的,String 和 Number 是不可變的。

Mutable is a type of variable that can be changed. In JavaScript, only objects and arrays are mutable, not primitive values.

常規的解決辦法可以深度克隆一個對象出來,再在新的對象上面做修改,以保證數據的可控性。

const foo = { a: 1 };const bar = _.cloneDeep(foo);bar.a = 2; // foo.a 仍為 1

cloneDeep 雖然可以解決這個問題,但在頻繁的深度克隆會造成內存浪費。淺克隆雖然會節省一部分內存,但不解決根本問題。

Immutable.js

目前 JavaScript 不存在原生的不可變 Object 與 Array,所以需要引入一個第三方庫來實現,這裡介紹 Facebook 推出的 Immutable.js,雖然它被認為是 React 性能優化中不可缺少的一部分,但它自身帶來的價值也小。

來看一段官網的 DEMO

const { Map } = require(immutable)const map1 = Map({ a: 1, b: 2, c: 3 })const map2 = map1.set(b, 50)map1.get(b) + " vs. " + map2.get(b) // 2 vs. 50

對比文章開頭的第一個栗子,可以看出通過 Immutable 生成的對象是不可變的,對其進行操作後會生成一個新的對象。

看到這裡,Immutable 只解決了上面對象引用造成的問題,但生成了一個新的對象會不會造成空間和性能上的損耗?

簡單闡述下 Immutable.js 在性能上的優勢:

節省存儲空間

Immutable 中,採用前綴樹(Trie)結構存儲對象。把一個 Immutable 生成的對象想像成一棵樹,樹上面的節點代表對象的某個屬性值。當我們要更新一個屬性值時,就是要重塑 Trie 樹中的一個節點,造成的影響就是當前這個屬性節點及其父節點們,其他分支上面的節點扔可以共享這個結構。

用網上的圖再說明下:

假設當前 Immutable 生成的一個對象結構是這樣的一棵樹

現在要對左下角的屬性節點【tea】做修改,產生的影響是

綠色節點表示受到了影響,需要生成新的節點。雖然對該對象的某個屬性修改後會生成一顆新的 Trie 樹,但未受到影響的屬性節點不會重新生成,引用關係不變,仍然可以結構共享。

這種存儲結構帶來的好處是,當對象比較大時,對某個屬性的更改並不會造成多大的性能開銷,同時又保證了對象的 不可變 特性。

提升對象比較的性能

基於 Trie 的存儲結構,兩個對象之間的比較不再需要深度比較。一旦對象發生變化,就會生成一個新的 Trie 樹,只需要淺比較即可,React 性能提升的其中一個手段就是它。

Trie 樹

Trie 樹中文名叫字典樹、前綴樹,它與字元的處理有關,事實也確實如此,它主要用途就是將對象、字元串整合成樹形。先來看一下由「南京」、「南京大學」、「南方」、「北京」、「京城」五個中文詞構成的 Trie 樹形:

這個樹裡面每一個方塊代表一個節點,其中 」Root」 表示根節點,不代表任何字元;漢子代表分支節點,數字代表葉子節點。除根節點外每一個節點都只包含一個字元。從根節點到葉子節點,路徑上經過的字元連接起來,構成一個詞。而葉子節點內的數字代表該詞在字典樹中所處的鏈路(字典中有多少個詞就有多少條鏈路),具有共同前綴的鏈路稱為串。除此之外,還需特彆強調 Trie 樹的以下幾個特點:

  1. 具有相同前綴的詞必須位於同一個串內;例如「南京」、「南方」兩個詞都有「南」這個前綴,那麼在 Trie 樹上只需構建一個「南」節點,「京」和「方」節點共用一個父節點即可,如此兩個詞便只需三個節點便可存儲,這在一定程度上減少了字典的存儲空間。
  2. Trie 樹中的詞只可共用前綴,不可共用詞的其他部分;例如「北京」、「京城」這兩個詞雖然前一個詞的後綴是後一個詞的前綴,但在樹形上必須是獨立的兩條鏈路,而不可以通過首尾交接構建這兩個詞,這也說明 Trie 樹僅能依靠公共前綴壓縮字典的存儲空間,並不能共享詞中的所有相同的字元。
  3. Trie 樹中任何一個完整的詞,都必須是從根節點開始至葉子節點結束,這意味著對一個詞進行檢索也必須從根節點開始,至葉子節點才算結束。

在 Trie 樹中搜索一個字元串,會從根節點出發,沿著某條鏈路向下逐字比對字元串的每個字元,直到抵達底部的葉子節點才能確認字元串為該詞,這種檢索方式具有以下兩個優點:

  1. 公共前綴的詞都位於同一個串內,查詞範圍因此被大幅縮小(比如首字不同的字元串,都會被排除)。
  2. Trie 樹實質是一個有限狀態自動機((Definite Automata, DFA),這就意味著從 Trie 樹的一個節點(狀態)轉移到另一個節點(狀態)的行為完全由狀態轉移函數控制,而狀態轉移函數本質上是一種映射,這意味著:逐字搜索 Trie 樹時,從一個字元到下一個字元比對是不需要遍歷該節點的所有子節點的。

確定的有限自動機 M 是一個五元組:

M = (Σ, Q, δ, q0, F)

其中,

Σ 是輸入符號的有窮集合;

Q 是狀態的有限集合;

δ 是 Q 與 Σ 的直積 Q × Σ 到Q (下一個狀態) 的映射。它支配著有限狀態控制的行為,有時也稱為狀態>轉移函數。

q0 ∈ Q 是初始狀態;

F 是終止狀態集合,F ? Q;

可以把DFA想像成一個單放機,插入一盤磁帶,隨著磁帶的轉動,DFA讀取一個符號,依靠狀態轉移函數>改變自己的狀態,同時磁帶轉到下一個字元。

常用 API

Immutable 中的 常用的 Map、Array、Set 等 API,使用習慣都與原生 JavaScript 幾乎一樣。不過要說明的是,為了避免無法區分原生 JavaScript 對象與 Immutable 生成的對象,Immutable 生成的對象最好以 $$ 開頭。

更全的 API 建議到此 facebook.github.io/immu

1. Map

const { Map } = require(immutable);const $$map1 = Map({ a: 1, b: 2, c: 3 });const $$map2 = $$map1.set(a, 11);

2. List

const { List } = require(immutable);const $$list1 = List([1, 2, 3]);const $$list2 = $$list1.set(0, newVal);const $$list3 = $$list1.push(4);

3. formJS

const { fromJS } = require(immutable);const $$nested = fromJS({ a: { b: { c: [ 3, 4, 5 ] } } });// Map { a: Map { b: Map { c: List [ 3, 4, 5 ] } } }

4. is

Immutable 對象可以使用 === 來比較,比較的是引用地址。如果想對兩個 Immutable 對象做 值比較,使用 is()

const { Map, is } = require(immutable);const $$map1 = Map({ a: 1, b: 2, c: 3 });const $$map2 = Map({ a: 1, b: 2, c: 3 });$$map1 !== $$map2 // two different instances are not reference-equal$$map1.equals($$map2) // but are value-equal if they have the same valuesis($$map1, $$map2) // alternatively can use the is() function

推薦閱讀:

jquery中img的load事件執行問題
從新的 Context API 看 React 應用設計模式
全面了解TCP/IP到HTTP
Koa源碼分析
前端日刊-2018.01.05

TAG:前端開發 |