源码细品针对源码中容易被忽视的细节点进行进一步剖析,写的会比较细,多来自笔者理解代码后分析后的思考,如果有考虑不到位或者判断有误的地方,请大家帮忙指出,谢谢啦。
UML关系图
上图是我画的类图关系,他们之间通过子属性赋值其他类实例化的方式形成关联,Hash、MapCache类具有相同名称的方法,使缓存读取方式具有一致性。
源码
Hash
/** Used to stand-in for `undefined` hash values. */
const HASH_UNDEFINED = '__lodash_hash_undefined__'
class Hash {
/**
* Creates a hash object.
*
* @private
* @constructor
* @param {Array} [entries] The key-value pairs to cache.
*/
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])
}
}
/**
* Removes all key-value entries from the hash.
*
* @memberOf Hash
*/
clear() {
this.__data__ = Object.create(null)
this.size = 0
}
/**
* Removes `key` and its value from the hash.
*
* @memberOf Hash
* @param {string} key The key of the value to remove.
* @returns {boolean} Returns `true` if the entry was removed, else `false`.
*/
delete(key) {
const result = this.has(key) && delete this.__data__[key]
this.size -= result ? 1 : 0
return result
}
/**
* Gets the hash value for `key`.
*
* @memberOf Hash
* @param {string} key The key of the value to get.
* @returns {*} Returns the entry value.
*/
get(key) {
const data = this.__data__
const result = data[key]
return result === HASH_UNDEFINED ? undefined : result
}
/**
* Checks if a hash value for `key` exists.
*
* @memberOf Hash
* @param {string} key The key of the entry to check.
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
*/
has(key) {
const data = this.__data__
return data[key] !== undefined
}
/**
* Sets the hash `key` to `value`.
*
* @memberOf Hash
* @param {string} key The key of the value to set.
* @param {*} value The value to set.
* @returns {Object} Returns the hash instance.
*/
set(key, value) {
const data = this.__data__
this.size += this.has(key) ? 0 : 1
data[key] = value === undefined ? HASH_UNDEFINED : value
return this
}
}
export default Hash
先创建一个实例hash
const hash = new Hash
// 等价于
const hash = new Hash()
在constructor
接受一个类型为Array的参数,并且结构假设为键值对二元数组[[key1, value1], [key2, value2]]
的形式,提供了初始化时批量存储键值对的方式,若hash实例中并没有传参,构建完成之后是一个类似下面这样的对象
hash = {
__data__: {},
size: 0
}
// 假设有传参 hash = new Hash([['a', 1], ['b', 2]])
hash = {
__data__: {
a: 1,
b: 2
},
size: 2
}
hash对象的__data__是通过Object.create(null)
创建而成的,是一个纯粹的空对象,在缓存的场景中访问速度会快些。
hash对象的size属性用于对缓存属性的计数,未成功缓存或删除的属性不会计算进去,故有了加减0或1的判断。
set是一种存储key-value值的方法,在set属性值时,如果用户set key但是没有设置对应的value值,Hash set内部给了一个字符串默认值HASH_UNDEFINED
,可表示被设置过key但未设置值的undefined,而不是取不到key值返回的undefined,区分的粒度更小。
Hash源码细节点在于这个HASH_UNDEFINED
的设置。
MapCache
import Hash from './Hash.js'
/**
* Gets the data for `map`.
*
* @private
* @param {Object} map The map to query.
* @param {string} key The reference key.
* @returns {*} Returns the map data.
*/
function getMapData({ __data__ }, key) {
const data = __data__
return isKeyable(key)
? data[typeof key === 'string' ? 'string' : 'hash']
: data.map
}
/**
* Checks if `value` is suitable for use as unique object key.
*
* @private
* @param {*} value The value to check.
* @returns {boolean} Returns `true` if `value` is suitable, else `false`.
*/
function isKeyable(value) {
const type = typeof value
return (type === 'string' || type === 'number' || type === 'symbol' || type === 'boolean')
? (value !== '__proto__')
: (value === null)
}
class MapCache {
/**
* Creates a map cache object to store key-value pairs.
*
* @private
* @constructor
* @param {Array} [entries] The key-value pairs to cache.
*/
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])
}
}
/**
* Removes all key-value entries from the map.
*
* @memberOf MapCache
*/
clear() {
this.size = 0
this.__data__ = {
'hash': new Hash,
'map': new Map,
'string': new Hash
}
}
/**
* Removes `key` and its value from the map.
*
* @memberOf MapCache
* @param {string} key The key of the value to remove.
* @returns {boolean} Returns `true` if the entry was removed, else `false`.
*/
delete(key) {
const result = getMapData(this, key)['delete'](key)
this.size -= result ? 1 : 0
return result
}
/**
* Gets the map value for `key`.
*
* @memberOf MapCache
* @param {string} key The key of the value to get.
* @returns {*} Returns the entry value.
*/
get(key) {
return getMapData(this, key).get(key)
}
/**
* Checks if a map value for `key` exists.
*
* @memberOf MapCache
* @param {string} key The key of the entry to check.
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
*/
has(key) {
return getMapData(this, key).has(key)
}
/**
* Sets the map `key` to `value`.
*
* @memberOf MapCache
* @param {string} key The key of the value to set.
* @param {*} value The value to set.
* @returns {Object} Returns the map cache instance.
*/
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
}
}
export default MapCache
MapCache
的存储思路和Hash是一致的,初始化时支持批量设置键值对,也可以使用set方法单独添加键值对,不过,MapCache中有两个函数值得注意:
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)
}
先看isKeyable
函数,判断键值对的key是否是keyable
,keyable
我理解是可作为键的key,回到getMapData
函数就可以判断出,这里是根据键key的类型做了一个分类,分类后存储在__data__
的不同的位置,还记得__data__
的内部结构吗?
this.__data__ = {
'hash': new Hash,
'map': new Map,
'string': new Hash
}
所以在存储时:
-
如果key类型是不可作为key值的,则放到
this.__data__
的map
属性下,存储空间是原生对象Map
,详情可见MDN map; -
如果key类型是可以作为key的(包含string、number、symbol、boolean这些基本数据类型)分为:
- 类型是string,放到
this.__data__
的string
属性下; - 类型不是string,放到
this.__data__
的hash
属性下。- (两者的存储类型都是上面提到过的Hash类型)
- 类型是string,放到
举个栗子🌰
const mapCache = new MapCache()
mapCache.set('666', '1')
mapCache.set(666, '2')
mapCache.set({}, '这是一个空对象')
console.log(mapCache.__data__)
// 输出
{
hash: {
'666': '2'
},
map: Map(1){
{} => '这是一个空对象'
}
string: {
'666': '1'
}
}
从上面的例子可以清晰的看出对key值分类的好处,可以有效的避免key值在toString()后的键key冲突导致值value被覆盖的情况。
SetCache
import MapCache from './MapCache.js'
/** Used to stand-in for `undefined` hash values. */
const HASH_UNDEFINED = '__lodash_hash_undefined__'
class SetCache {
/**
* Creates an array cache object to store unique values.
*
* @private
* @constructor
* @param {Array} [values] The values to cache.
*/
constructor(values) {
let index = -1
const length = values == null ? 0 : values.length
this.__data__ = new MapCache
while (++index < length) {
this.add(values[index])
}
}
/**
* Adds `value` to the array cache.
*
* @memberOf SetCache
* @alias push
* @param {*} value The value to cache.
* @returns {Object} Returns the cache instance.
*/
add(value) {
this.__data__.set(value, HASH_UNDEFINED)
return this
}
/**
* Checks if `value` is in the array cache.
*
* @memberOf SetCache
* @param {*} value The value to search for.
* @returns {boolean} Returns `true` if `value` is found, else `false`.
*/
has(value) {
return this.__data__.has(value)
}
}
SetCache.prototype.push = SetCache.prototype.add
export default SetCache
SetCache
构成比较简单,可以把它比作一个去重后的,可缓存的数组,它的内部属性__data__
是MapCache
的实例,由于MapCache
内部是key-value键值对的存储,它的键key为数组每一项的值,value取的是默认的展位字符HASH_UNDEFINED
。
结语
缓存对象在lodash很多地方都用到了,用于提升方法执行的性能,在数据量较大(如数组length>=120)时尤为有用。若是我自己封装一个缓存函数,可能代码不会这么优雅,分类也不会那么细,多数时候用一个空对象去实现,不过也看场景,有时候确实一个简单的对象就可以解决问题了。
一起读源码,与君共勉。