lodash源碼分析之緩存方式的選擇

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

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

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

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

前言

在《lodash源碼分析之Hash緩存》和《lodash源碼分析之List緩存》介紹了 lodash 的兩種緩存方式,這兩種緩存方式都實現了和 Map 一致的數據管理接口,其中 List 緩存只在不支持 Map 的環境中使用,那什麼時候使用 Hash 緩存,什麼時候使用 Map 或者 List 緩存呢?這就是 MapCache 類所須要作的事情。github

緩存方式的選擇

從以前的分析能夠看出,Hash 緩存徹底能夠用 List 緩存或者 Map 來代替,爲何 lodash 不乾脆統一用一種緩存方式呢?數組

緣由是在數據量較大時,對象的存取比 Map 或者數組的性能要好。緩存

所以,ladash 在可以用 Hash 緩存時,都儘可能使用 Hash 緩存,而可否使用 Hash 緩存的關鍵是 key 的類型。微信

如下便爲 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 一致的數據管理接口,以下:

依賴

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)

最後,全部文章都會同步發送到微信公衆號上,歡迎關注,歡迎提意見:

做者:對角另外一面

相關文章
相關標籤/搜索