Lodash 源码阅读-MapCache
概述
MapCache 是 Lodash 内部使用的一个高效键值对缓存结构,它根据键的类型将数据分发到不同的存储结构中,实现了高效的数据存取。它是 Lodash 中许多缓存相关功能的基础实现。
前置学习
- Hash:用于存储字符串和基本类型键的缓存结构
- ListCache:用于存储不可哈希键的缓存结构
- Map:原生 Map 对象,用于高效存储引用类型键
- getMapData:MapCache 内部路由函数,根据键类型选择正确的存储结构
- isKeyable:判断键是否可直接用于哈希表存储的辅助函数
源码实现
javascript
/**
* Creates a map cache object to store key-value pairs.
*
* @private
* @constructor
* @param {Array} [entries] The key-value pairs to cache.
*/
function MapCache(entries) {
var index = -1,
length = entries == null ? 0 : entries.length;
this.clear();
while (++index < length) {
var entry = entries[index];
this.set(entry[0], entry[1]);
}
}
/**
* Removes all key-value entries from the map.
*
* @private
* @name clear
* @memberOf MapCache
*/
function mapCacheClear() {
this.size = 0;
this.__data__ = {
hash: new Hash(),
map: new (Map || ListCache)(),
string: new Hash(),
};
}
/**
* Removes `key` and its value from the map.
*
* @private
* @name delete
* @memberOf MapCache
* @param {string} key The key of the value to remove.
* @returns {boolean} Returns `true` if the entry was removed, else `false`.
*/
function mapCacheDelete(key) {
var result = getMapData(this, key)["delete"](key);
this.size -= result ? 1 : 0;
return result;
}
/**
* Gets the map value for `key`.
*
* @private
* @name get
* @memberOf MapCache
* @param {string} key The key of the value to get.
* @returns {*} Returns the entry value.
*/
function mapCacheGet(key) {
return getMapData(this, key).get(key);
}
/**
* Checks if a map value for `key` exists.
*
* @private
* @name has
* @memberOf MapCache
* @param {string} key The key of the entry to check.
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
*/
function mapCacheHas(key) {
return getMapData(this, key).has(key);
}
/**
* Sets the map `key` to `value`.
*
* @private
* @name set
* @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.
*/
function mapCacheSet(key, value) {
var data = getMapData(this, key),
size = data.size;
data.set(key, value);
this.size += data.size == size ? 0 : 1;
return this;
}
// Add methods to `MapCache`.
MapCache.prototype.clear = mapCacheClear;
MapCache.prototype["delete"] = mapCacheDelete;
MapCache.prototype.get = mapCacheGet;
MapCache.prototype.has = mapCacheHas;
MapCache.prototype.set = mapCacheSet;
实现思路
MapCache 的核心设计理念是根据键的类型将数据分发到最适合的存储结构中,以实现最高效的存取性能。它使用三种不同的存储结构:
- hash: 使用 Hash 对象存储数字、布尔值、符号等基本类型键
- string: 使用 Hash 对象专门处理字符串类型的键
- map: 使用 Map 或 ListCache 处理复杂的引用类型键
通过这种分类存储的策略,MapCache 能够高效处理各种类型的键,无论是基本类型还是对象引用。所有操作都会通过 getMapData 函数路由到正确的存储结构,然后委托给对应结构的方法来完成。
源码解析
构造函数
javascript
function MapCache(entries) {
var index = -1,
length = entries == null ? 0 : entries.length;
this.clear();
while (++index < length) {
var entry = entries[index];
this.set(entry[0], entry[1]);
}
}
构造函数接收一个可选的 entries
参数,这是一个键值对数组。它的工作流程是:
- 初始化索引变量
index
为 -1 - 获取 entries 的长度,如果 entries 为 null 或 undefined,则长度为 0
- 调用
this.clear()
初始化内部存储结构 - 遍历 entries 数组,依次调用
this.set()
设置每个键值对
例如:
javascript
// 创建一个空的 MapCache
const emptyCache = new MapCache();
// 内部结构: __data__ = { hash: Hash实例, map: Map实例, string: Hash实例 }, size = 0
// 创建一个带有初始数据的 MapCache
const cache = new MapCache([
["name", "John"],
[42, "answer"],
[{ id: 1 }, "object"],
]);
// 键'name'存储在__data__.string中
// 键42存储在__data__.hash中
// 键{id: 1}存储在__data__.map中
clear 方法
javascript
function mapCacheClear() {
this.size = 0;
this.__data__ = {
hash: new Hash(),
map: new (Map || ListCache)(),
string: new Hash(),
};
}
clear 方法重置缓存,将 size 设为 0,并为三种不同类型的键创建新的存储结构:
'hash'
: 一个新的 Hash 实例,用于存储基本类型键'string'
: 一个新的 Hash 实例,专门存储字符串键'map'
: 一个新的 Map 实例(如果环境支持),或者一个 ListCache 实例(作为降级方案)
这种分类存储的设计是 MapCache 高效处理不同类型键的核心。
注意这里的 new (Map || ListCache)
语法,它会尝试使用原生 Map,如果不可用则回退到 ListCache 实现。
delete 方法
javascript
function mapCacheDelete(key) {
var result = getMapData(this, key)["delete"](key);
this.size -= result ? 1 : 0;
return result;
}
delete 方法从缓存中移除指定键的键值对:
- 调用
getMapData
获取存储该键的正确存储结构 - 调用该存储结构的 delete 方法删除键
- 如果删除成功(result 为 true),则将 MapCache 的 size 减 1
- 返回删除操作的结果
例如:
javascript
cache.delete("name"); // 从 string 存储中删除
cache.delete(42); // 从 hash 存储中删除
cache.delete({ id: 1 }); // 从 map 存储中删除(通过引用比较)
get 方法
javascript
function mapCacheGet(key) {
return getMapData(this, key).get(key);
}
get 方法获取指定键的值:
- 调用
getMapData
获取存储该键的正确存储结构 - 调用该存储结构的 get 方法并返回结果
这是一个非常简洁的实现,只是将操作委托给正确的子存储结构。
has 方法
javascript
function mapCacheHas(key) {
return getMapData(this, key).has(key);
}
has 方法检查缓存中是否存在指定的键:
- 调用
getMapData
获取存储该键的正确存储结构 - 调用该存储结构的 has 方法并返回结果
同样是一个简单的委托实现。
set 方法
javascript
function mapCacheSet(key, value) {
var data = getMapData(this, key),
size = data.size;
data.set(key, value);
this.size += data.size == size ? 0 : 1;
return this;
}
set 方法用于设置或更新键值对:
- 调用
getMapData
获取存储该键的正确存储结构 - 记录操作前该存储结构的 size
- 调用该存储结构的 set 方法设置键值对
- 更新 MapCache 的 size:如果子存储结构的 size 增加了(表示添加了新键),则 MapCache 的 size 也增加 1
- 返回 MapCache 实例以支持链式调用
这个实现确保了 MapCache 的 size 属性与其所有子存储结构中的键数量之和保持一致。
应用场景
MapCache 在 Lodash 内部被广泛使用,主要有以下几个场景:
- 高性能缓存 :用于实现
_.memoize
等缓存功能 - 去重操作 :用于
_.uniq
、_.intersection
等需要快速查找的操作 - 复杂对象键存储:当需要使用对象作为键时,MapCache 提供了高效的解决方案
- Stack 数据结构:当 Stack 需要处理大量数据时,会从 ListCache 升级到 MapCache
- SetCache 的实现基础:SetCache 内部使用 MapCache 存储唯一值
总结
MapCache 是 Lodash 中一个精巧的缓存实现,它通过根据键的类型选择最佳存储结构来实现高效的数据访问。它的设计体现了以下几个重要的软件设计原则:
- 分而治之:将不同类型的键分配到专门的存储结构中处理
- 适配器模式:通过 getMapData 函数将操作路由到正确的底层实现
- 降级策略:当环境不支持 Map 时自动降级到 ListCache
- 接口一致性:为所有操作提供统一的接口,隐藏内部的复杂性
从性能角度看,MapCache 的主要优势在于:
- 字符串和基本类型键可以直接通过哈希表 O(1) 时间复杂度访问
- 引用类型键通过 Map 高效存储,避免了传统对象键的字符串转换问题
- 通过细分存储结构,减少了每个子结构中的元素数量,提高了查找效率
MapCache 的实现给我们的启示是:在处理多样化的数据时,可以根据数据的特性选择不同的数据结构,通过组合它们来获得最佳的性能。这种根据键类型选择最佳存储结构的思想,可以应用到我们自己的缓存系统和数据结构设计中。