Lodash 源码阅读-SetCache
概述
SetCache 是 Lodash 中的一个内部缓存结构,专门用于存储唯一值的集合。它本质上是一个集合(Set)的实现,通过使用 MapCache 作为底层存储,为集合操作提供高效的添加和查找功能。在 Lodash 内部,SetCache 主要用于实现数组去重、交集、差集等需要高性能唯一值检查的场景。
前置学习
依赖函数
- MapCache:SetCache 内部使用 MapCache 作为存储结构,MapCache 提供了高效的键值存储和查找能力
- HASH_UNDEFINED :Lodash 内部的特殊标记值,用于表示值存在但是内容为
undefined
源码实现
javascript
/**
* Creates an array cache object to store unique values.
*
* @private
* @constructor
* @param {Array} [values] The values to cache.
*/
function SetCache(values) {
var index = -1,
length = values == null ? 0 : values.length;
this.__data__ = new MapCache();
while (++index < length) {
this.add(values[index]);
}
}
/**
* Adds `value` to the array cache.
*
* @private
* @name add
* @memberOf SetCache
* @alias push
* @param {*} value The value to cache.
* @returns {Object} Returns the cache instance.
*/
function setCacheAdd(value) {
this.__data__.set(value, HASH_UNDEFINED);
return this;
}
/**
* Checks if `value` is in the array cache.
*
* @private
* @name has
* @memberOf SetCache
* @param {*} value The value to search for.
* @returns {number} Returns `true` if `value` is found, else `false`.
*/
function setCacheHas(value) {
return this.__data__.has(value);
}
// Add methods to `SetCache`.
SetCache.prototype.add = SetCache.prototype.push = setCacheAdd;
SetCache.prototype.has = setCacheHas;
实现思路
SetCache 的核心设计思路是利用 MapCache 高效的键值查找能力,将集合元素存储为 MapCache 的键,而值统一设为一个特殊的标记值(HASH_UNDEFINED)。这样实现了一个高效的集合结构,可以快速添加元素和检查元素是否存在。SetCache 只提供了两个核心操作:添加元素(add/push)和检查元素是否存在(has),这也符合集合结构的基本功能需求。整个设计专注于优化集合操作的性能,尤其是处理大量数据时的查找效率。
源码解析
构造函数
javascript
function SetCache(values) {
var index = -1,
length = values == null ? 0 : values.length;
this.__data__ = new MapCache();
while (++index < length) {
this.add(values[index]);
}
}
构造函数接收一个可选的 values
数组参数,用于初始化缓存:
- 首先初始化索引变量
index = -1
,用于后续遍历 - 获取数组长度,如果
values
为 null 或 undefined,则长度为 0 - 创建一个新的 MapCache 实例作为内部存储(
this.__data__
) - 使用 while 循环遍历
values
数组,将每个元素添加到缓存中
这里使用 ++index
的前置递增操作,与初始值 -1 配合,确保从第一个元素(索引 0)开始处理。
示例:
javascript
// 创建一个空的 SetCache
const emptyCache = new SetCache();
// 内部结构: __data__ = new MapCache
// 创建一个带有初始值的 SetCache
const cache = new SetCache([1, 2, 3, 3, 4]);
// 因为是集合,所以重复的 3 只会被存储一次
// 内部结构中的 MapCache 存储了键 1, 2, 3, 4,值均为 HASH_UNDEFINED
add 方法
javascript
function setCacheAdd(value) {
this.__data__.set(value, HASH_UNDEFINED);
return this;
}
add 方法用于向集合中添加一个值:
- 调用内部 MapCache 实例的 set 方法,将
value
作为键,HASH_UNDEFINED 作为值存储 - 返回 this 以支持链式调用
值得注意的是,这里使用 MapCache 的 key-value 结构来实现 Set,键是我们要存储的实际值,而值则统一为 HASH_UNDEFINED。这样设计的好处是可以利用 MapCache 高效的键查找能力,无论是基本类型还是引用类型的值都能高效处理。
由于 MapCache 内部会自动处理重复键的情况(更新值而不增加新条目),这也自然实现了集合的唯一性特征,同一个值不会被添加多次。
示例:
javascript
const cache = new SetCache();
cache.add("a");
cache.add(42);
cache.add({ id: 1 });
// 重复添加相同值不会产生效果
cache.add("a");
// 内部 MapCache 仍然只有三个键值对
has 方法
javascript
function setCacheHas(value) {
return this.__data__.has(value);
}
has 方法检查一个值是否在集合中:
- 直接调用内部 MapCache 实例的 has 方法
- 返回检查结果(布尔值)
这个实现非常简洁,只是将操作委托给内部的 MapCache 实例。MapCache 的 has 方法会根据键的类型(字符串、数字或引用类型)选择最优的存储结构进行查找,这确保了 SetCache 的查找操作也同样高效。
示例:
javascript
const cache = new SetCache([1, 2, 3]);
console.log(cache.has(2)); // true
console.log(cache.has(4)); // false
const obj = { id: 5 };
cache.add(obj);
console.log(cache.has(obj)); // true
console.log(cache.has({ id: 5 })); // false,因为是不同的对象引用
原型方法绑定
javascript
SetCache.prototype.add = SetCache.prototype.push = setCacheAdd;
SetCache.prototype.has = setCacheHas;
这段代码将函数绑定到 SetCache 的原型上:
- 将 setCacheAdd 函数绑定为 SetCache 原型的 add 方法
- 同时将 setCacheAdd 函数也绑定为 push 方法(作为 add 的别名)
- 将 setCacheHas 函数绑定为 has 方法
提供 push 作为 add 的别名是一个有趣的设计,让 SetCache 在某些场景下可以与数组保持类似的接口,提高了 API 的友好性和一致性。
总结
SetCache 是 Lodash 内部实现的一个高效集合数据结构,它基于 MapCache 提供了快速的元素添加和查找能力。虽然它的 API 非常简单(只有 add/push 和 has 两个方法),但在处理大量数据的去重、交集、差集等操作时扮演着关键角色。
从设计角度看,SetCache 展示了以下几个优秀的设计原则:
- 单一职责原则:SetCache 专注于提供集合的核心功能(添加和检查),不包含其他复杂操作
- 组合优于继承:通过内部使用 MapCache 实例而非继承它,SetCache 获得了更大的灵活性和封装性
- 接口简洁性:只提供必要的接口,简化了使用复杂度
- 性能优化:通过利用 MapCache 的高效键存储机制,优化了集合操作的性能
通过学习 SetCache 的实现,我们可以看到如何利用已有的数据结构构建新的抽象,以及如何为特定用例优化性能。这种将通用数据结构组合成专用工具的设计思路,在我们自己的代码中也值得借鉴。SetCache 虽小,但体现了数据结构设计的精妙之处,特别是在处理大量数据时如何权衡时间和空间复杂度。