每個人心裡都有一團火,路過的人只看到煙。
——《至愛梵高·星空之謎》
本文為讀 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 決定使用快取方式的流程:
首先,判斷 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
所做的事情有點像函式過載,其呼叫方式和 Hash
、Map
及 ListCache
一致。
new MapCache([
[`key`, `value`],
[{key: `An Object Key`}, 1],
[Symbol(),2]
])
複製程式碼
所返回的結果如下:
{
size: 3,
__data__: {
string: {
...
},
hash: {
...
},
map: {
...
}
}
}
複製程式碼
可以看到,__data__
里根據 key
的型別分成了 string
、hash
和 map
三種型別來儲存資料。其中 string
和 hash
都是 Hash
的例項,而 map
則是 map
或 ListCache
的例項。
介面設計
MapCache
同樣實現了跟 Map
一致的資料管理介面,如下:
依賴
import Hash from `./Hash.js`
import ListCache from `./ListCache.js`
複製程式碼
原始碼分析
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])
}
}
複製程式碼
構造器跟 Hash
和 ListCache
一模一樣,都是先呼叫 clear
方法,然後呼叫 set
方法,往快取中加入初始資料。
clear
clear() {
this.size = 0
this.__data__ = {
`hash`: new Hash,
`map`: new (Map || ListCache),
`string`: new Hash
}
}
複製程式碼
clear
是為了清空快取。
這裡值得注意的是 __data__
屬性,使用 hash
、string
和 map
來儲存不同型別的快取資料,它們之間的區別上面已經論述清楚。
這裡也可以清晰地看到,如果在支援 Map
的環境中,會優先使用 Map
,而不是 ListCache
。
has
has(key) {
return getMapData(this, key).has(key)
}
複製程式碼
has
用來判斷是否已經有快取資料,如果快取資料已經存在,則返回 true
。
這裡呼叫了 getMapData
方法,獲取到對應的快取例項(Hash
、Map
或者 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)
最後,所有文章都會同步傳送到微信公眾號上,歡迎關注,歡迎提意見:
作者:對角另一面