lodash原始碼分析之快取方式的選擇

對角發表於2019-03-01

每個人心裡都有一團火,路過的人只看到煙。

——《至愛梵高·星空之謎》

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

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

前言

在《lodash原始碼分析之Hash快取》和《lodash原始碼分析之List快取》介紹了 lodash 的兩種快取方式,這兩種快取方式都實現了和 Map 一致的資料管理介面,其中 List 快取只在不支援 Map 的環境中使用,那何時使用 Hash 快取,何時使用 Map 或者 List 快取呢?這就是 MapCache 類所需要做的事情。

快取方式的選擇

從之前的分析可以看出,Hash 快取完全可以用 List 快取或者 Map 來代替,為什麼 lodash 不乾脆統一用一種快取方式呢?

原因是在資料量較大時,物件的存取比 Map 或者陣列的效能要好。

因此,ladash 在能夠用 Hash 快取時,都儘量使用 Hash 快取,而能否使用 Hash 快取的關鍵是 key 的型別。

以下便為 lodash 決定使用快取方式的流程:

lodash原始碼分析之快取方式的選擇

首先,判斷 key 的型別,以是否為 string/number/symbol/boolean 型別為成兩撥,如果是以上的型別,再判斷 key 是否等於 __proto__ ,如果不是 __proto__ ,則使用 Hash 快取。不能為 __proto__ 的原因是,大部分 JS 引擎都以這個屬性來儲存物件的原型。

如果不是以上的型別,則判斷 key 是否為 null,如果為 null ,則依然使用 Hash 快取,其餘的則使用 Map 或者 List 快取。

從上面的流程圖還可以看到,在可以用 Hash 來快取的 key 中,還以是否為 string 型別分成了兩個 Hash 物件來快取資料,為什麼要這樣呢?

我們都知道,物件的 key 如果不是字串或者 Symbol 型別時,會轉換成字串的形式,因此如果快取的資料中同時存在像數字 1 和字串 `1` 時,資料都會儲存在字串 `1` 上。這兩個不同的鍵值,最後獲取的都是同一份資料,這明顯是不行的,因此需要將要字串的 key 和其他需要轉換型別的 key 分開兩個 Hash 物件儲存。

作用與用法

MapCache 所做的事情有點像函式過載,其呼叫方式和 HashMapListCache 一致。

new MapCache([
  [`key`, `value`],
  [{key: `An Object Key`}, 1],
  [Symbol(),2]
])
複製程式碼

所返回的結果如下:

{
  size: 3,
  __data__: {
    string: {
      ... 
    },
    hash: {
      ...
    },
    map: {
      ...  
    }
  }
}
複製程式碼

可以看到,__data__ 里根據 key 的型別分成了 stringhashmap 三種型別來儲存資料。其中 stringhash 都是 Hash 的例項,而 map 則是 mapListCache 的例項。

介面設計

MapCache 同樣實現了跟 Map 一致的資料管理介面,如下:

lodash原始碼分析之快取方式的選擇

依賴

import Hash from `./Hash.js`
import ListCache from `./ListCache.js`
複製程式碼

lodash原始碼分析之Hash快取

lodash原始碼分析之List快取

原始碼分析

function getMapData({ __data__ }, key) {
  const data = __data__
  return isKeyable(key)
    ? data[typeof key == `string` ? `string` : `hash`]
    : data.map
}

function isKeyable(value) {
  const type = typeof value
  return (type == `string` || type == `number` || type == `symbol` || type == `boolean`)
    ? (value !== `__proto__`)
    : (value === null)
}

class MapCache {

  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.size = 0
    this.__data__ = {
      `hash`: new Hash,
      `map`: new (Map || ListCache),
      `string`: new Hash
    }
  }

  delete(key) {
    const result = getMapData(this, key)[`delete`](key)
    this.size -= result ? 1 : 0
    return result
  }

  get(key) {
    return getMapData(this, key).get(key)
  }

  has(key) {
    return getMapData(this, key).has(key)
  }

  set(key, value) {
    const data = getMapData(this, key)
    const size = data.size

    data.set(key, value)
    this.size += data.size == size ? 0 : 1
    return this
  }
}
複製程式碼

是否使用Hash

function isKeyable(value) {
  const type = typeof value
  return (type == `string` || type == `number` || type == `symbol` || type == `boolean`)
    ? (value !== `__proto__`)
  : (value === null)
}
複製程式碼

這個函式用來判斷是否使用 Hash 快取。返回 true 表示使用 Hash 快取,返回 false 則使用 Map 或者 ListCache 快取。

這個在流程圖上已經解釋過,不再作詳細的解釋。

獲取對應快取方式的例項

function getMapData({ __data__ }, key) {
  const data = __data__
  return isKeyable(key)
    ? data[typeof key == `string` ? `string` : `hash`]
    : data.map
}
複製程式碼

這個函式根據 key 來獲取儲存了該 key 的快取例項。

__data__ 即為 MapCache 例項中的 __data__ 屬性的值。

如果使用的是 Hash 快取,則型別為字串時,返回 __data__ 中的 string 屬性的值,否則返回 hash 屬性的值。這兩者都為 Hash 例項。

否則返回 map 屬性的值,這個可能是 Map 例項或者 ListCache 例項。

constructor

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])
  }
}
複製程式碼

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

clear

clear() {
  this.size = 0
  this.__data__ = {
    `hash`: new Hash,
    `map`: new (Map || ListCache),
    `string`: new Hash
  }
}
複製程式碼

clear 是為了清空快取。

這裡值得注意的是 __data__ 屬性,使用 hashstringmap 來儲存不同型別的快取資料,它們之間的區別上面已經論述清楚。

這裡也可以清晰地看到,如果在支援 Map 的環境中,會優先使用 Map ,而不是 ListCache

has

has(key) {
  return getMapData(this, key).has(key)
}
複製程式碼

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

這裡呼叫了 getMapData 方法,獲取到對應的快取例項(HashMap 或者 ListCache 的例項),然後呼叫的是對應例項中的 has 方法。

set

set(key, value) {
  const data = getMapData(this, key)
  const size = data.size

  data.set(key, value)
  this.size += data.size == size ? 0 : 1
  return this
}
複製程式碼

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

這裡除了呼叫對應的快取例項的 set 方法來維護快取的值外,還需要維護自身的 size 屬性,如果增加值,則加 1

get

get(key) {
  return getMapData(this, key).get(key)
}
複製程式碼

get 方法是從快取中取值。

同樣是呼叫對應的快取例項中的 get 方法。

delete

delete(key) {
  const result = getMapData(this, key)[`delete`](key)
  this.size -= result ? 1 : 0
  return result
}
複製程式碼

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

同樣是呼叫對應快取例項中的 delete 方法,如果刪除成功,則需要將自身的 size 的值減少 1

參考

License

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

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

lodash原始碼分析之快取方式的選擇

作者:對角另一面

相關文章