【lodash】源码细品 - Hash、MapCache、SetCache缓存

207 阅读1分钟

源码细品针对源码中容易被忽视的细节点进行进一步剖析,写的会比较细,多来自笔者理解代码后分析后的思考,如果有考虑不到位或者判断有误的地方,请大家帮忙指出,谢谢啦。

UML关系图

cache.png

上图是我画的类图关系,他们之间通过子属性赋值其他类实例化的方式形成关联,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是否是keyablekeyable我理解是可作为键的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类型)

举个栗子🌰

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)时尤为有用。若是我自己封装一个缓存函数,可能代码不会这么优雅,分类也不会那么细,多数时候用一个空对象去实现,不过也看场景,有时候确实一个简单的对象就可以解决问题了。

一起读源码,与君共勉。