lodash原始碼分析之List快取

對角發表於2019-02-21

昨日我沿著河岸/漫步到/蘆葦彎腰喝水的地方

順便請煙囪/在天空為我寫一封長長的信

潦是潦草了些/而我的心意/則明亮亦如你窗前的燭光/稍有曖昧之處/勢所難免/因為風的緣故

——洛夫《因為風的緣故》

本文為讀 lodash 原始碼的第七篇,後續文章會更新到這個倉庫中,歡迎 star:pocket-lodash

gitbook也會同步倉庫的更新,gitbook地址:pocket-lodash

作用與用法

在之前的《lodash原始碼分析之Hash快取》介紹過用 Hash 做快取的情況,在這篇文章中介紹過,lodash 是想要實現和 Map 一樣的介面。

Hash 其實是用物件來做快取,但是物件有一個侷限,它的 key 只能是字串或者 Symbol 型別,但是 Map 是支援各種型別的值來作為 key,因此 Hash 快取無法完全模擬 Map 的行為,當遇到 key 為陣列、物件等型別時,Hash 就無能為力了。

因此,在不支援 Map 的環境下,lodash 實現了 ListCache 來模擬,ListCache 本質上是使用一個二維陣列來儲存資料。

ListCache 的呼叫方式和 Hash 一致:

new ListCache([
  [{key: 'An Object Key'}, 1],
  [['An Array Key'],2],
  [function(){console.log('A Function Key')},3]
])
複製程式碼

返回的結果如下:

{
  size: 3,
  __data__: [
    [{key: 'An Object Key'}, 1],
    [['An Array Key'],2],
    [function(){console.log('A Function Key')},3]
  ]
}
複製程式碼

結構和 Hash 類似,但是 __data__ 變成了陣列。

介面設計

ListCache 的介面與 Hash 一樣,同樣實現了 Map 的資料管理介面。

lodash原始碼分析之List快取

依賴

import assocIndexOf from './assocIndexOf.js'
複製程式碼

lodash原始碼分析之自減的兩種形式

原始碼分析

class ListCache {

  constructor(entries) {
    let index = -1
    const length = entries == null ? 0 : entries.length

    this.clear()
    while (++index < length) {
      const entry = entries[index]
      this.set(entry[0], entry[1])
    }
  }

  clear() {
    this.__data__ = []
    this.size = 0
  }

  delete(key) {
    const data = this.__data__
    const index = assocIndexOf(data, key)

    if (index < 0) {
      return false
    }
    const lastIndex = data.length - 1
    if (index == lastIndex) {
      data.pop()
    } else {
      data.splice(index, 1)
    }
    --this.size
    return true
  }

  get(key) {
    const data = this.__data__
    const index = assocIndexOf(data, key)
    return index < 0 ? undefined : data[index][1]
  }

  has(key) {
    return assocIndexOf(this.__data__, key) > -1
  }

  set(key, value) {
    const data = this.__data__
    const index = assocIndexOf(data, key)

    if (index < 0) {
      ++this.size
      data.push([key, value])
    } else {
      data[index][1] = value
    }
    return this
  }
}
複製程式碼

constructor

構造器跟 Hash 一模一樣,都是先呼叫 clear 方法,然後呼叫 set 方法,往快取中加入初始資料。

這裡呼叫 clear 方法並不是說為了清除資料,還沒開始使用這個類,肯定是沒有資料的,而是為了初始化 __data__size 這兩個屬性。

clear

clear() {
  this.__data__ = []
  this.size = 0
}
複製程式碼

clear 是為了清空快取。

其實就是將容器 __data__ 設定成空陣列,在 Hash 中是設定為空物件,將快取數量 size 設定為 0

has

has(key) {
  return assocIndexOf(this.__data__, key) > -1
}
複製程式碼

has 用來判斷是否已經有快取資料,如果快取資料已經存在,則返回 true

在之前的文章中已經介紹過,assocIndexOf 檢測的是對應 key[key,value] 陣列在二維陣列中的索引,其行為跟 indexOf 一致,不存在於二維陣列中時,返回 -1 ,否則返回索引值。因此可以用是否大於 -1 來判斷指定 key 的資料是否已經被快取。

set

set(key, value) {
  const data = this.__data__
  const index = assocIndexOf(data, key)

  if (index < 0) {
    ++this.size
    data.push([key, value])
  } else {
    data[index][1] = value
  }
  return this
}
複製程式碼

set 用來增加或者更新需要快取的值。set 的時候需要同時維護 size 和快取的值。

has 一樣,呼叫 assocIndexOf 找到指定 key 的索引值,如果小於 0 ,則表明指定的 key 尚未快取,需要將快取數量 size1 ,然後將快取資料加入到 this.__data__ 的末尾。

否則更新 value 即可。

強迫症看到 has 用大於 -1 來判斷,而這裡用小於 0 來判斷可能會相當難受。

get

get(key) {
  const data = this.__data__
  const index = assocIndexOf(data, key)
  return index < 0 ? undefined : data[index][1]
}
複製程式碼

get 方法是從快取中取值。

如果快取中存在值,則返回快取中的值,否則返回 undefined

delete

delete(key) {
  const data = this.__data__
  const index = assocIndexOf(data, key)

  if (index < 0) {
    return false
  }
  const lastIndex = data.length - 1
  if (index == lastIndex) {
    data.pop()
  } else {
    data.splice(index, 1)
  }
  --this.size
  return true
}
複製程式碼

delete 方法用來刪除指定 key 的快取。成功刪除返回 true, 否則返回 false。 刪除操作同樣需要維護 size 屬性。

首先呼叫 assocIndexOf 來找到快取的索引。

如果索引小於 0 ,表明沒有快取,刪除不成功,直接返回 false

如果要刪除的快取是快取中的最後一項,則直接呼叫 pop 方法,將快取刪除,否則將呼叫 splice 方法將對應位置的快取刪除。

為什麼不直接都用 splice 來刪除資料呢?因為 pop 的效能比 splice 好,我簡單測了一下,大概快 17% 左右。

有興趣的可以看下 popsplice 的規範,splice 要比 pop 做的事情要多。

從這裡又看出了 lodash 對效能的極致追求。

最後將快取數量 size 減少 1

參考

  1. Set 和 Map 資料結構
  2. MDN: 使用物件
  3. ECMAScript5.1中文版 + ECMAScript3 + ECMAScript(合集)

License

署名-非商業性使用-禁止演繹 4.0 國際 (CC BY-NC-ND 4.0)

最後,所有文章都會同步傳送到微信公眾號上,歡迎關注,歡迎提意見:

lodash原始碼分析之List快取

作者:對角另一面

相關文章