国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

深入探究immutable.js的實現(xiàn)機制(一)

zhangfaliang / 3178人閱讀

摘要:本文會集合多方資料以及我自己的一些理解,深入一些探究實現(xiàn)機制。位分區(qū)實際上是數(shù)字分區(qū)的一個子集,所有以的整數(shù)次冪,,,,為基數(shù)的數(shù)字分區(qū)前綴樹,都可以轉(zhuǎn)為位分區(qū)。其實舉個例子最好理解比如數(shù)字的二進(jìn)制形式是,這是一個位的二進(jìn)制數(shù)。

Immutable.js 采用了持久化數(shù)據(jù)結(jié)構(gòu)結(jié)構(gòu)共享,保證每一個對象都是不可變的,任何添加、修改、刪除等操作都會生成一個新的對象,且通過結(jié)構(gòu)共享等方式大幅提高性能。
網(wǎng)上已經(jīng)有很多文章簡單介紹了 Immutable.js 的原理,但基本都是淺嘗輒止,我也是搜了很久沒找到針對 Immutable.js 原理的相對深入詳細(xì)的文章,中英文都沒有,針對 Clojure 或 Go 中持久化數(shù)據(jù)結(jié)構(gòu)實現(xiàn)的文章倒是有一些。本文會集合多方資料以及我自己的一些理解,深入一些探究 Immutable.js 實現(xiàn)機制。文章可能會分2-3篇完成。

Immutable.js 部分參考了 Clojure 中的PersistentVector的實現(xiàn)方式,并有所優(yōu)化和取舍,本文的一些內(nèi)容也是基于它,想了解的可以閱讀這里(共五篇,這是第一篇)
簡單的例子

在深入研究前,我們先看個簡單的例子:

let map1 = Immutable.Map({});

for (let i = 0; i < 800; i++) {
  map1 = map1.set(Math.random(), Math.random());
}

console.log(map1);

這段代碼先后往map里寫入了800對隨機生成的key和value。我們先看一下控制臺的輸出結(jié)果,對它的數(shù)據(jù)結(jié)構(gòu)有個大致的認(rèn)知(粗略掃一眼就行了):

可以看到這是一個樹的結(jié)構(gòu),子節(jié)點以數(shù)組的形式放在nodes屬性里,nodes的最大長度似乎是32個。這里的bitmap涉及到對于樹寬度的壓縮,這些后面會說。
其中一個節(jié)點層層展開后長這樣:

這個ValueNode存的就是一組值了,entry[0]是key,entry[1]是value。

大致看個形狀就行了,下面來由淺入深研究一下。

基本原理

我們先看下維基對于持久化數(shù)據(jù)結(jié)構(gòu)的定義:

In computing, a persistent data structure is a data structure that always preserves the previous version of itself when it is modified.

通俗點解釋就是,對于一個持久化數(shù)據(jù)結(jié)構(gòu),每次修改后我們都會得到一個新的版本,且舊版本可以完好保留。

Immutable.js 用樹實現(xiàn)了持久化數(shù)據(jù)結(jié)構(gòu),先看下圖這顆樹:

假如我們要在g下面插入一個節(jié)點h,如何在插入后讓原有的樹保持不變?最簡單的方法當(dāng)然是重新生成一顆樹:

但這樣做顯然是很低效的,每次操作都需要生成一顆全新的樹,既費時又費空間,因而有了如下的優(yōu)化方案:

我們新生成一個根節(jié)點,對于有修改的部分,把相應(yīng)路徑上的所有節(jié)點重新生成,對于本次操作沒有修改的部分,我們可以直接把相應(yīng)的舊的節(jié)點拷貝過去,這其實就是結(jié)構(gòu)共享。這樣每次操作同樣會獲得一個全新的版本(根節(jié)點變了,新的a!==舊的a),歷史版本可以完好保留,同時也節(jié)約了空間和時間。
至此我們發(fā)現(xiàn),用樹實現(xiàn)持久化數(shù)據(jù)結(jié)構(gòu)還是比較簡單的,Immutable.js提供了多種數(shù)據(jù)結(jié)構(gòu),比如回到開頭的例子,一個map如何成為持久化數(shù)據(jù)結(jié)構(gòu)呢?

Vector Trie

實際上對于一個map,我們完全可以把它視為一顆扁平的樹,與上文實現(xiàn)持久化數(shù)據(jù)結(jié)構(gòu)的方式一樣,每次操作后生成一個新的對象,把舊的值全都依次拷貝過去,對需要修改或添加的屬性,則重新生成。這其實就是Object.assign,然而這樣顯然效率很低,有沒有更好的方法呢?
在實現(xiàn)持久化數(shù)據(jù)結(jié)構(gòu)時,Immutable.js 參考了Vector Trie這種數(shù)據(jù)結(jié)構(gòu)(其實更準(zhǔn)確的叫法是persistent bit-partitioned vector triebitmapped vector trie,這是Clojure里使用的一種數(shù)據(jù)結(jié)構(gòu),Immutable.js 里的相關(guān)實現(xiàn)與其很相似),我們先了解下它的基本結(jié)構(gòu)。
假如我們有一個 map ,key 全都是數(shù)字(當(dāng)然你也可以把它理解為數(shù)組){0: "banana", 1: "grape", 2: "lemon", 3: "orange", 4: "apple"},為了構(gòu)造一棵二叉Vector Trie,我們可以先把所有的key轉(zhuǎn)換為二進(jìn)制的形式:{"000": "banana", "001": "grape", "010": "lemon", "011": "orange", "100": "apple"},然后如下圖構(gòu)建Vector Trie

可以看到,Vector Trie的每個節(jié)點是一個數(shù)組,數(shù)組里有01兩個數(shù),表示一個二進(jìn)制數(shù),所有值都存在葉子節(jié)點上,比如我們要找001的值時,只需順著0 0 1找下來,即可得到grape。那么想實現(xiàn)持久化數(shù)據(jù)結(jié)構(gòu)當(dāng)然也不難了,比如我們想添加一個5: "watermelon"

可見對于一個 key 全是數(shù)字的map,我們完全可以通過一顆Vector Trie來實現(xiàn)它,同時實現(xiàn)持久化數(shù)據(jù)結(jié)構(gòu)。如果key不是數(shù)字怎么辦呢?轉(zhuǎn)成數(shù)字就行了。 Immutable.js 實現(xiàn)了一個hash函數(shù),可以把一個值轉(zhuǎn)換成相應(yīng)數(shù)字。
這里為了簡化,每個節(jié)點數(shù)組長度僅為2,這樣在數(shù)據(jù)量大的時候,樹會變得很深,查詢會很耗時,所以可以擴大數(shù)組的長度,Immutable.js 選擇了32。為什么不是31?40?其實數(shù)組長度必須是2的整數(shù)次冪,這里涉及到實現(xiàn)Vector Trie時的一個優(yōu)化,接下來我們先研究下這點。

數(shù)字分區(qū)(Digit partitioning)

數(shù)字分區(qū)指我們把一個 key 作為數(shù)字對應(yīng)到一棵前綴樹上,正如上節(jié)所講的那樣。
假如我們有一個 key 9128,以 7 為基數(shù),即數(shù)組長度是 7,它在Vector Trie里是這么表示的:

需要5層數(shù)組,我們先找到3這個分支,再找到5,之后依次到0。為了依次得到這幾個數(shù)字,我們可以預(yù)先把9128轉(zhuǎn)為7進(jìn)制的35420,但其實沒有這個必要,因為轉(zhuǎn)為 7 進(jìn)制形式的過程就是不斷進(jìn)行除法并取余得到每一位上的數(shù),我們無須預(yù)先轉(zhuǎn)換好,類似的操作可以在每一層上依次執(zhí)行。
運用進(jìn)制轉(zhuǎn)換相關(guān)的知識,我們可以采用這個方法key / radixlevel - 1 % radix得到每一位的數(shù)(為了簡便,本文除代碼外所有/符號皆表示除法且向下取整),其中radix是每層數(shù)組的長度,即轉(zhuǎn)換成幾進(jìn)制,level是當(dāng)前在第幾層,即第幾位數(shù)。比如這里key9128radix7,一開始level5,通過這個式子我們可以得到第一層的數(shù)3
代碼實現(xiàn)如下:

const RADIX = 7;

function find(key) {
  let node = root; // root是根節(jié)點,在別的地方定義了

  // depth是當(dāng)前樹的深度。這種計算方式跟上面列出的式子是等價的,但可以避免多次指數(shù)計算
  for (let size = Math.pow(RADIX, (depth - 1)); size > 1; size /= RADIX) {
    node = node[Math.floor(key / size) % RADIX];
  }

  return node[key % RADIX];
}
位分區(qū)(Bit Partitioning)

顯然,以上數(shù)字分區(qū)的方法是有點耗時的,在每一層我們都要進(jìn)行兩次除法一次取模,顯然這樣并不高效,位分區(qū)就是對其的一種優(yōu)化。
位分區(qū)實際上是數(shù)字分區(qū)的一個子集,所有以2的整數(shù)次冪(2,4,8,16,32...)為基數(shù)的數(shù)字分區(qū)前綴樹,都可以轉(zhuǎn)為位分區(qū)。基于一些位運算相關(guān)的知識,我們就能避免一些耗時的計算。
數(shù)字分區(qū)把 key 拆分成一個個數(shù)字,而位分區(qū)把 key 分成一組組 bit。比如一個 32 路的前綴樹,數(shù)字分區(qū)的方法是把 key 以 32 為基數(shù)拆分(實際上就是32進(jìn)制),而位分區(qū)是把它以 5bit 拆分,實際上就是把 32 進(jìn)制數(shù)的每一位看做 5 個 bit ,或者說把 32 進(jìn)制數(shù)看做2進(jìn)制進(jìn)行操作,這樣原本的很多計算就可以用更高效的位運算的方式代替。因為現(xiàn)在基數(shù)是 32,即radix為 32,所以前面的式子現(xiàn)在是key / 32level - 1 % 32,而 32 又可以寫作25,那么該式子可以轉(zhuǎn)成這樣key / 25 × (level - 1) % 25。根據(jù)位運算相關(guān)的知識我們知道a / 2n === a >>> n a % 2n === a & 2n-1
其實舉個例子最好理解:比如數(shù)字666666的二進(jìn)制形式是10100 01011 00001 01010,這是一個20位的二進(jìn)制數(shù)。如果我們要得到第二層那五位數(shù)01011,我們可以先把它右移>>>(左側(cè)補0)10位,得到00000 00000 10100 01011,再&一下00000 00000 00000 11111,就得到了01011
這樣我們可以得到下面的代碼:

const SHIFT = 5;
const WIDTH = 1 << SHIFT, //  32
const MASK = WIDTH - 1; // 31,即11111

function find(key) {
  let node = root; 

  for (let shift = (depth - 1) * SHIFT; shift > 0; shift -= SHIFT) {
    node = node[(key >>> shift) & MASK];
  }

  return node[key & MASK];
}

這樣我們每次查找的速度就會得到提升。可以看一張圖進(jìn)行理解,為了簡化展示,假設(shè)我們只有2位分區(qū)即4路的前綴樹,對于626,我們的查找過程如下:

626的二進(jìn)制形式是10 01 11 00 10,所以通過以上的位運算,我們便依次得到了1001...

源碼

說了這么多,我們看一下 Immutable.js 的源碼吧。雖然具體的代碼較長,但主要看一下查找的部分就夠了,這是Vector Trie的核心。

get(shift, keyHash, key, notSetValue) {
  if (keyHash === undefined) {
    keyHash = hash(key);
  }
  const idx = (shift === 0 ? keyHash : keyHash >>> shift) & MASK;
  const node = this.nodes[idx];
  return node
    ? node.get(shift + SHIFT, keyHash, key, notSetValue)
    : notSetValue;
}

可以看到, Immutable.js 也正是采用了位分區(qū)的方式,通過位運算得到當(dāng)前數(shù)組的 index 選擇相應(yīng)分支。
不過它的實現(xiàn)方式與上文所講的有一點不同,上文中對于一個 key ,我們是“正序”存儲的,比如上圖那個626的例子,我們是從根節(jié)點往下依次按照10 01 11 00 10去存儲,而 Immutable.js 里則是“倒序”,按照10 00 11 01 10存儲。所以通過源碼這段你會發(fā)現(xiàn) Immutable.js 查找時先得到的是 key 末尾的 SHIFT 個 bit ,然后再得到它們之前的 SHIFT 個 bit ,依次往前下去,而前面我們的代碼是先得到 key 開頭的 SHIFT 個 bit,依次往后。
至于為什么這么做,我一開始也沒理解,但仔細(xì)想想這的確是最好的一種方式了,用這種方式的根本原因是key的大小(二進(jìn)制長度)不固定,不固定的原因又是為了減小計算量,同時也能減小空間占用并讓樹更“平衡”。仔細(xì)思考一下的話,你應(yīng)該能理解。關(guān)于這塊內(nèi)容,如果有時間我會放到之后的文章里說。

時間復(fù)雜度

因為采用了結(jié)構(gòu)共享,在添加、修改、刪除操作后,我們避免了將 map 中所有值拷貝一遍,所以特別是在數(shù)據(jù)量較大時,這些操作相比Object.assign有明顯提升。
然而,查詢速度似乎減慢了?我們知道 map 里根據(jù) key 查找的速度是O(1),這里由于變成了一棵樹,查詢的時間復(fù)雜度變成了O(log N),準(zhǔn)確說是O(log32 N)
等等, 32 叉樹?這棵樹可不是一般地寬啊,Javascript里對象可以擁有的key的最大數(shù)量一般不會超過232個(ECMA-262第五版里定義了JS里由于數(shù)組的長度本身是一個 32 位數(shù),所以數(shù)組長度不應(yīng)大于 232 - 1 ,JS里對象的實現(xiàn)相對復(fù)雜,但大部分功能是建立在數(shù)組上的,所以在大部分場景下對象里 key 的數(shù)量不會超過 232 - 1。相關(guān)討論見這里),這樣就可以把查找的時間復(fù)雜度當(dāng)做是“O(log32 232)”,差不多就是“O(log 7)”,所以我們可以認(rèn)為在實際運用中,5bit (32路)的 Vector Trie 查詢的時間復(fù)雜度是常數(shù)級的,32 叉樹就是用了空間換時間。
空間...這個 32 叉樹占用的空間也太大了吧?即便只有三層,我們也會有超過32 × 32 × 32 = 32768個節(jié)點。當(dāng)然 Immutable.js 在具體實現(xiàn)時肯定不會傻乎乎的占用這么大空間,它對樹的高度和寬度都做了“壓縮”,此外,還對操作效率進(jìn)行了其它一些優(yōu)化,比如對 list 進(jìn)行了“tail優(yōu)化”。相關(guān)內(nèi)容下一篇再討論

如果文章里有什么問題歡迎指正。

該文章是我正在更新的深入探究immutable.js系列的第一篇,我花了不少功夫才完成這篇文章,如果對你有幫助,希望能點個贊~

然后也請期待下一篇吧~預(yù)計一共會分2-3篇寫完。該文章里有不懂的地方?jīng)]關(guān)系,之后的文章會討論更多內(nèi)容,同時會有助于對該文章的理解。

第二篇更新到這里了:https://juejin.im/post/5ba4a6...

參考:
https://hypirion.com/musings/...
https://io-meter.com/2016/09/...
https://cdn.oreillystatic.com...
https://michael.steindorfer.n...
https://github.com/facebook/i...

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/97786.html

相關(guān)文章

  • Immutable

    摘要:如果實現(xiàn)了結(jié)構(gòu)共享,每次的新值共享內(nèi)部結(jié)構(gòu)以大幅減少內(nèi)存占用。這意味著,如果對一個進(jìn)行賦值次,并不會創(chuàng)建倍大小的內(nèi)存占用數(shù)據(jù)。消除了流經(jīng)系統(tǒng)的精神負(fù)擔(dān)。代價是編寫風(fēng)格將顛覆式的完全不同。會帶來很多無必要的渲染并成為性能瓶頸。 Part01 Immutable由何而生 說immutable之前,首先看下什么是mutable。js在原生創(chuàng)建數(shù)據(jù)類型即是mutable,可變的。const只是...

    Coly 評論0 收藏0
  • 讀懂immutable-jsMap數(shù)據(jù)結(jié)構(gòu)

    摘要:一向量字典樹字典樹,一種用空間換取時間的樹形數(shù)據(jù)結(jié)構(gòu),主要特點是利用字符串的公共前綴來挺升查詢性能。還有最終的數(shù)組表示的真實存儲的鍵值,存儲了,存儲了。這其中還有一種節(jié)點進(jìn)行了沖突的處理。 本文受深入探究Immutable.js的實現(xiàn)機制這篇文章啟發(fā),結(jié)合自己對Map源碼的解讀,談?wù)勎覍mmutable-js中map數(shù)據(jù)結(jié)構(gòu)的理解,若有不正確的地方,歡迎指正。 一、Vector Tr...

    jone5679 評論0 收藏0
  • react進(jìn)階漫談

    摘要:父組件向子組件之間非常常見,通過機制傳遞即可。我們應(yīng)該聽說過高階函數(shù),這種函數(shù)接受函數(shù)作為輸入,或者是輸出一個函數(shù),比如以及等函數(shù)。在傳遞數(shù)據(jù)的時候,我們可以用進(jìn)一步提高性能。 本文主要談自己在react學(xué)習(xí)的過程中總結(jié)出來的一些經(jīng)驗和資源,內(nèi)容邏輯參考了深入react技術(shù)棧一書以及網(wǎng)上的諸多資源,但也并非完全照抄,代碼基本都是自己實踐,主要為平時個人學(xué)習(xí)做一個總結(jié)和參考。 本文的關(guān)鍵...

    neuSnail 評論0 收藏0
  • react進(jìn)階漫談

    摘要:父組件向子組件之間非常常見,通過機制傳遞即可。我們應(yīng)該聽說過高階函數(shù),這種函數(shù)接受函數(shù)作為輸入,或者是輸出一個函數(shù),比如以及等函數(shù)。在傳遞數(shù)據(jù)的時候,我們可以用進(jìn)一步提高性能。 本文主要談自己在react學(xué)習(xí)的過程中總結(jié)出來的一些經(jīng)驗和資源,內(nèi)容邏輯參考了深入react技術(shù)棧一書以及網(wǎng)上的諸多資源,但也并非完全照抄,代碼基本都是自己實踐,主要為平時個人學(xué)習(xí)做一個總結(jié)和參考。 本文的關(guān)鍵...

    wyk1184 評論0 收藏0
  • react進(jìn)階漫談

    摘要:父組件向子組件之間非常常見,通過機制傳遞即可。我們應(yīng)該聽說過高階函數(shù),這種函數(shù)接受函數(shù)作為輸入,或者是輸出一個函數(shù),比如以及等函數(shù)。在傳遞數(shù)據(jù)的時候,我們可以用進(jìn)一步提高性能。 本文主要談自己在react學(xué)習(xí)的過程中總結(jié)出來的一些經(jīng)驗和資源,內(nèi)容邏輯參考了深入react技術(shù)棧一書以及網(wǎng)上的諸多資源,但也并非完全照抄,代碼基本都是自己實踐,主要為平時個人學(xué)習(xí)做一個總結(jié)和參考。 本文的關(guān)鍵...

    junnplus 評論0 收藏0

發(fā)表評論

0條評論

最新活動
閱讀需要支付1元查看
<