探索vue源碼之緩存篇

vue.js入坑也有了小半年的時間了,圈子裏一直流傳着其源碼優雅、簡潔的傳說。
最近的一次技術分享會,同事分享vue.js源碼的緩存部分,鄙人將其整理出來,與你們一塊兒學習javascript

1、從鏈表提及

首先咱們來看一下鏈表的定義:vue

鏈表(Linked list)是一種常見的基礎數據結構,是一種線性表,可是並不會按線性的順序存儲數據,而是在每個節點裏存到下一個節點的指針(Pointer)java

其中的雙向鏈表是咱們今天的主角:git

雙向鏈表也叫雙鏈表。雙向鏈表中不只有指向後一個節點的指針,還有指向前一個節點的指針。這樣能夠從任何一個節點訪問前一個節點,固然也能夠訪問後一個節點,以致整個鏈表。通常是在須要大批量的另外儲存數據在鏈表中的位置的時候用。github

圖示以下(圖片來自維基百科-鏈表):
圖片描述算法

想象一羣人手拉手站成一排,除了隊頭跟隊尾,能夠根據每一個人的左手以及右手找到排在其左邊或者右邊的人,這也能夠當作一種雙向鏈表數組

JavaScript中,咱們能夠經過對象的屬性來實現雙向鏈表。緩存

而在vue.js中,做者正是利用相似雙向鏈表的方式實現緩存的利用數據結構

2、LRU算法

在緩存中,利用相似雙向鏈表來管理緩存並不難的。難的是如何更加高效的管理緩存,如何在緩存達到其最大內存空間,刪除程序中最不經常使用的變量,而不是隨機刪除,形成最經常使用的變量被誤刪的狀況。函數

vue.js中採用LRU算法來實現緩存的高效管理。

LRULeast Recently Used的簡稱,具體內容能夠查看GitHub,其有如下優勢:

  1. 基於雙向鏈表改變緩存對象中entry的排序,複雜度低

  2. 緩存對象有一個head(最近最少使用的項)和一個tail(最近最多使用的項)

  3. headtail都是entry,一個entry可能會有一個newer entry以及一個older entry(雙向連接,older entry更接近headnewer entry更接近tail

  4. 使用一個key就能夠遍歷這個緩存對象,也就意味着只有o(1)的複雜度,內存消耗很是小

能夠經過下面的圖來更好的理解LRU算法:

entry             entry             entry             entry        
    ______            ______            ______            ______       
   | head |.newer => |      |.newer => |      |.newer => | tail |      
   |  A   |          |  B   |          |  C   |          |  D   |      
   |______| <= older.|______| <= older.|______| <= older.|______|      
                                                                       
removed  <--  <--  <--  <--  <--  <--  <--  <--  <--  <--  <--  added

若是緩存達到最大,那麼每次只須要將head刪除就好了,保證了刪除的項是最不經常使用的項

仍是拿站成一排的人來舉例。

有兩個指示牌,上面分別寫着tail以及headhead指向隊伍的第一我的,tail指向隊伍的最後一我的。

假設隊伍有10我的,按照隊伍的排列從隊首到隊尾依次編號a b c d ··· jhead指向atail指向j

下面分紅五種狀況來講明隊伍的變化:

  1. 若是叫到a(使用了數組裏面第一個變量),就將a放到隊尾,再手拉手從新組成一個新的隊伍。並將原來指向jtail如今指向a。再讓原來指向ahead指向如今隊伍的第一我的b

  2. 若是叫到b c d ··· i之間任何一我的,則將其從隊伍中抽出,放到隊尾,從新排隊,再改變tail的指向爲這我的

  3. 若是叫到j,則保持隊伍不變

  4. 隊伍達到最大人數,則去掉head指向的編號a,並改變head指向編號b,再在隊尾增長一我的,假定編號爲k,最後則將tail指向編號k

  5. 隊伍沒有達到最大人數,須要增長隊伍人數。只須要在隊尾增長編號爲k的人。再將tail指向編號k

3、源碼分析

咱們能夠經過一張圖來先簡單理解做者的數據結構:
圖片描述

做者在caches對象的_keymap裏面保存所須要緩存的變量,經過older以及newer這兩個屬性來實現雙向鏈表。older指向其前一個對象,newer指向其後一個對象。經過這兩個屬性,將緩存中的變量鏈接起來。

以上圖舉例:
緩存caches這個對象中保存了三個變量:key1key2key3

  • header指向key1

  • tail指向key2

指向以下:

key1              key2              key3       
        ______            ______            ______       
       | head |.newer => |      |.newer => | tail |      
       |      |          |      |          |      |      
       |______| <= older.|______| <= older.|______|

下面咱們來看做者對這些數據的處理所使用的方法

文件位置:src/cache.js

首先export構造函數Cache

export default function Cache (limit) {
  // 標識當前緩存數組的大小
  this.size = 0
  // 標識緩存數組能達到的最大長度
  this.limit = limit
  // head(最不經常使用的項),tail(最經常使用的項)所有初始化爲undefined
  this.head = this.tail = undefined
  this._keymap = Object.create(null)
}

接下來做者在Cache的原型鏈上面分別定義了:

  1. put:在緩存中加入一個key-value對象,若是緩存數組已經達到最大值,則返回被刪除的entry,即head,不然返回undefined

  2. shift:在緩存數組中移除最少使用的entry,即head,返回被刪除的entry。若是緩存數組爲空,則返回undefined

  3. get:將key爲傳入參數的緩存對象標識爲最常使用的entry,即tail,並調整雙向鏈表,返回改變後的tail。若是不存在key爲傳入參數的緩存對象,則返回undefined

a) get:

Cache.prototype.get = function (key, returnEntry) {
  var entry = this._keymap[key]
  // 若是查找不到含有`key`這個屬性的緩存對象
  if (entry === undefined) return
  // 若是查找到的緩存對象已是 tail (最近使用過的)
  if (entry === this.tail) {
    return returnEntry
      ? entry
      : entry.value
  }
  // HEAD--------------TAIL
  //   <.older   .newer>
  //  <--- add direction --
  //   A  B  C  <D>  E
  if (entry.newer) {
  // 處理 newer 指向
    if (entry === this.head) {
      // 若是查找到的緩存對象是 head (最近最少使用過的)
      // 則將 head 指向原 head 的 newer 所指向的緩存對象
      this.head = entry.newer
    }
    // 將所查找的緩存對象的下一級的 older 指向所查找的緩存對象的older所指向的值
    // 例如:A B C D E
    // 若是查找到的是D,那麼將E指向C,再也不指向D
    entry.newer.older = entry.older // C <-- E.
  }
  if (entry.older) {
  // 處理 older 指向
    // 若是查找到的是D,那麼C指向E,再也不指向D
    entry.older.newer = entry.newer // C. --> E
  }
  // 處理所查找到的對象的 newer 以及 older 指向
  entry.newer = undefined // D --x
  // older指向以前使用過的變量,即D指向E
  entry.older = this.tail // D. --> E
  if (this.tail) {
    // 將E的newer指向D
    this.tail.newer = entry // E. <-- D
  }
  // 改變 tail 爲D 
  this.tail = entry
  return returnEntry
    ? entry
    : entry.value
}

b) put:

Cache.prototype.put = function (key, value) {
  var removed

  var entry = this.get(key, true)
  // 若是不存在 key 這樣屬性的緩存對象,才能調用 put 方法
  if (!entry) {
    if (this.size === this.limit) {
    // 若是緩存數組達到上限,則先刪除 head 指向的緩存對象
      removed = this.shift()
    }
    // 初始化賦值
    entry = {
      key: key
    }
    this._keymap[key] = entry
    if (this.tail) {
    // 若是存在tail(緩存數組的長度不爲0),將tail指向新的 entry
      this.tail.newer = entry
      entry.older = this.tail
    } else {
    // 若是緩存數組的長度爲0,將head指向新的entry
      this.head = entry
    }
    this.tail = entry
    this.size++
  }
  entry.value = value

  return removed
}

c) shift

Cache.prototype.shift = function () {
  var entry = this.head
  if (entry) {
    // 刪除 head ,並改變指向
    this.head = this.head.newer
    this.head.older = undefined
    entry.newer = entry.older = undefined
    // 同步更新 _keymap 裏面的屬性值
    this._keymap[entry.key] = undefined
    // 同步更新 緩存數組的長度
    this.size--
  }
  return entry
}

4、後記

從整個的代碼來看,須要學習的不只僅是LRU算法,做者的對於Object的處理方式也值的咱們評味一番。

沒有選擇去遍歷entry,選擇經過在Cache內增長一個_keymap屬性,經過這個屬性來管理entry,實現keynewerolder狀態的分離,減小代碼的複雜度

5、附

  1. 源碼版本爲v1.0.26

  2. 主要內容來自愛屋吉屋FE團隊的技術分享會

相關文章
相關標籤/搜索