Immutable.js 源碼解析 --List 類型

1、存儲圖解

我如下面這段代碼爲例子,畫出這個List的存儲結構:node

let myList = [];
for(let i=0;i<1100;i++) {
    myList[i] = i;
}
debugger;//能夠在這裏打個斷點調試
let immutableList = Immutable.List(myList)
debugger;
console.log(immutableList.set(1000, 'Remm'));
debugger;
console.log(immutableList.get(1000));

圖片描述

2、vector trie 的構建過程

咱們用上面的代碼爲例子一步一步的解析。首先是把原生的list轉換爲inmutable的list 類型:數組

export class List extends IndexedCollection {
  // @pragma Construction

  constructor(value) { // 此時的value就是上面的myList數組
    const empty = emptyList();
    if (value === null || value === undefined) {//判斷是否爲空
      return empty;
    }
    if (isList(value)) {//判斷是否已是imutable的list類型
      return value;
    }
    const iter = IndexedCollection(value);//序列化數組
    const size = iter.size;
    if (size === 0) {
      return empty;
    }
    assertNotInfinite(size);
    if (size > 0 && size < SIZE) { // 判斷size是否超過32
      return makeList(0, size, SHIFT, null, new VNode(iter.toArray()));
    }
    return empty.withMutations(list => {
      list.setSize(size);
      iter.forEach((v, i) => list.set(i, v));
    });
  }

  。。。。。。

}

首先會建立一個空的list數據結構

let EMPTY_LIST;
export function emptyList() {
  return EMPTY_LIST || (EMPTY_LIST = makeList(0, 0, SHIFT));
}

SHIFT的值爲5,export const SHIFT = 5; // Resulted in best performance after ______?函數

再繼續看makeList,能夠清晰看到 List 的主要部分:this

function makeList(origin, capacity, level, root, tail, ownerID, hash) {
  const list = Object.create(ListPrototype);
  list.size = capacity - origin;// 數組的長度
  list._origin = origin;// 數組的起始位置 通常是0
  list._capacity = capacity;// 數組容量 等於 size
  list._level = level;//樹的深度,爲0時是葉子結點。默認值是5,存儲指數部分,用於方便位運算,增長一個深度,level值+5
  list._root = root;// trie樹實現
  list._tail = tail;// 32個爲一組,存放最後剩餘的數據 其實就是 %32
  list.__ownerID = ownerID;
  list.__hash = hash;
  list.__altered = false;
  return list;
}

將傳入的數據序列化spa

// ArraySeq
iter = {
size: 數組的length,
_array: 傳入數組的引用
}

判斷size是否超過32,size > 0 && size < SIZE 這裏 SIZE : export const SIZE = 1 << SHIFT;
即 32。若沒有超過32,全部數據都放在_tail中。debug

_root 和 _tail 裏面的數據又有如下結構:調試

// @VNode class
constructor(array, ownerID) {
  this.array = array;
  this.ownerID = ownerID;
}

能夠這樣調試查看:code

let myList = [];
for(let i=0;i<30;i++) {
    myList[i] = i;
}
debugger;//能夠在這裏打個斷點調試
console.log(Immutable.List(myList));

size若是超過32orm

return empty.withMutations(list => {
    list.setSize(size);//構建樹的結構 主要是計算出樹的深度
    iter.forEach((v, i) => list.set(i, v));//填充好數據
});
export function withMutations(fn) {
  const mutable = this.asMutable();
  fn(mutable);
  return mutable.wasAltered() ? mutable.__ensureOwner(this.__ownerID) : this;
}

list.setSize(size)中有一個重要的方法setListBounds,下面咱們主要看這個方法如何構建這顆樹
這個方法最主要的做用是 肯定 list的level

function setListBounds(list, begin, end) {

  ......
  
  const newTailOffset = getTailOffset(newCapacity);

  // New size might need creating a higher root.
  // 是否須要增長數的深度 把 1 左移 newLevel + SHIFT 位 至關於 1 * 2 ^ (newLevel + SHIFT)
  // 以 size爲 1100 爲例子 newTailOffset的值爲1088 第一次 1088 > 2 ^ 10 樹增長一層深度
  // 第二次 1088 < 2 ^ 15 跳出循環 newLevel = 10
  while (newTailOffset >= 1 << (newLevel + SHIFT)) {
    newRoot = new VNode(
      newRoot && newRoot.array.length ? [newRoot] : [],
      owner
    );
    newLevel += SHIFT;
  }

  ......
}
function getTailOffset(size) {
    // (1100 - 1) / 2^5 % 2^5 = 1088
    return size < SIZE ? 0 : (((size - 1) >>> SHIFT) << SHIFT);
}

通過 list.setSize(size);構建好的結構

圖片描述

3、set 方法

iter.forEach((v, i) => list.set(i, v));這裏是將iter中的_array填充到list

這裏主要仍是看看set方法如何設置數據

set(index, value) {
    return updateList(this, index, value);
}
function updateList(list, index, value) {
  ......
  if (index >= getTailOffset(list._capacity)) {
    newTail = updateVNode(newTail, list.__ownerID, 0, index, value, didAlter);
  } else {
    newRoot = updateVNode(
      newRoot,
      list.__ownerID,
      list._level,
      index,
      value,
      didAlter
    );
  }

  ......

}
function updateVNode(node, ownerID, level, index, value, didAlter) {
  // 根據 index 和 level 計算 數據set的位置在哪
  const idx = (index >>> level) & MASK;

  // 利用遞歸 一步一步的尋找位置 直到找到最終的位置
  if (level > 0) {
    const lowerNode = node && node.array[idx];
    const newLowerNode = updateVNode(
      lowerNode,
      ownerID,
      level - SHIFT,
      index,
      value,
      didAlter
    );
    ......
    // 把node節點的array複製一份生成一個新的節點newNode editableVNode函數見下面源碼
    newNode = editableVNode(node, ownerID);
    // 回溯階段將 子節點的引用賦值給本身
    newNode.array[idx] = newLowerNode;
    return newNode;
  }
  ......
  newNode = editableVNode(node, ownerID);
  // 當遞歸到葉子節點 也就是level <= 0 將值放到這個位置
  newNode.array[idx] = value;
  ......
  return newNode;
}
function editableVNode(node, ownerID) {
  if (ownerID && node && ownerID === node.ownerID) {
    return node;
  }
  return new VNode(node ? node.array.slice() : [], ownerID);
}

下面咱們看看運行了一次set(0,0)的結果

圖片描述

整個結構構建完以後

圖片描述

下面咱們接着看剛剛咱們構建的list set(1000, 'Remm'),其實全部的set的源碼上面已經解析過了,咱們再來溫習一下。

調用上面的set方法,index=1000,value='Remm'。調用updateList,繼而調用updateVNode。經過const idx = (index >>> level) & MASK;計算要尋找的節點的位置(在這個例子中,idx的值依次是0->31->8)。 不斷的遞歸查找,當 level <= 0 到達遞歸的終止條件,其實就是達到樹的葉子節點,此時經過newNode = editableVNode(node, ownerID);建立一個新的節點,而後 newNode.array[8] = 'Remm'。接着就是開始回溯,在回溯階段,本身把本身克隆一個,newNode = editableVNode(node, ownerID);,注意這裏克隆的只是引用,因此不是深拷貝。而後再將idx位置的更新了的子節點從新賦值,newNode.array[idx] = newLowerNode;,這樣沿着路徑一直返回,更新路徑上的每一個節點,最後獲得一個新的根節點。

更新後的list:

圖片描述

4、get 方法

瞭解完上面的list構建和set,咱們再來看 immutableList.get(1000) 源碼就是小菜一碟了。

get(index, notSetValue) {
    index = wrapIndex(this, index);
    if (index >= 0 && index < this.size) {
      index += this._origin;
      const node = listNodeFor(this, index);
      return node && node.array[index & MASK];
    }
    return notSetValue;
  }
function listNodeFor(list, rawIndex) {
  if (rawIndex >= getTailOffset(list._capacity)) {
    return list._tail;
  }
  if (rawIndex < 1 << (list._level + SHIFT)) {
    let node = list._root;
    let level = list._level;
    while (node && level > 0) {
      // 循環查找節點所在位置
      node = node.array[(rawIndex >>> level) & MASK];
      level -= SHIFT;
    }
    return node;
  }
}

5、tire 樹 的優勢

來一張從網上盜來的圖:
圖片描述

這種樹的數據結構(tire 樹),保證其拷貝引用的次數降到了最低,就是經過極端的方式,大大下降拷貝數量,一個擁有100萬條屬性的對象,淺拷貝須要賦值 99.9999萬次,而在 tire 樹中,根據其訪問的深度,只有一個層級只須要拷貝 31 次,這個數字不隨着對象屬性的增長而增大。而隨着層級的深刻,會線性增長拷貝數量,但因爲對象訪問深度不會特別高,10 層已經幾乎見不到了,所以最多拷貝300次,速度仍是很是快的。

我上面所解析的狀況有 構建、修改、查詢。其實還有 添加 和 刪除。
其實Immutable.js 部分參考了 Clojure 中的PersistentVector的實現方式。因此能夠看看下面這篇文章:

https://hypirion.com/musings/...

相關文章
相關標籤/搜索