Lodash 源码阅读-ListCache
概述
ListCache 是 Lodash 内部使用的一个简单键值对缓存结构,基于数组实现。它主要用于存储少量的键值对数据,提供了高效的增删改查操作。
前置学习
- assocIndexOf:用于在数组中查找指定键的索引位置
- eq:用于比较两个值是否相等(使用 SameValueZero 算法)
源码实现
javascript
/**
* Creates a list cache object.
*
* @private
* @constructor
* @param {Array} [entries] The key-value pairs to cache.
*/
function ListCache(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 list cache.
*
* @private
* @name clear
* @memberOf ListCache
*/
function listCacheClear() {
this.__data__ = [];
this.size = 0;
}
/**
* Removes `key` and its value from the list cache.
*
* @private
* @name delete
* @memberOf ListCache
* @param {string} key The key of the value to remove.
* @returns {boolean} Returns `true` if the entry was removed, else `false`.
*/
function listCacheDelete(key) {
var data = this.__data__,
index = assocIndexOf(data, key);
if (index < 0) {
return false;
}
var lastIndex = data.length - 1;
if (index == lastIndex) {
data.pop();
} else {
splice.call(data, index, 1);
}
--this.size;
return true;
}
/**
* Gets the list cache value for `key`.
*
* @private
* @name get
* @memberOf ListCache
* @param {string} key The key of the value to get.
* @returns {*} Returns the entry value.
*/
function listCacheGet(key) {
var data = this.__data__,
index = assocIndexOf(data, key);
return index < 0 ? undefined : data[index][1];
}
/**
* Checks if a list cache value for `key` exists.
*
* @private
* @name has
* @memberOf ListCache
* @param {string} key The key of the entry to check.
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
*/
function listCacheHas(key) {
return assocIndexOf(this.__data__, key) > -1;
}
/**
* Sets the list cache `key` to `value`.
*
* @private
* @name set
* @memberOf ListCache
* @param {string} key The key of the value to set.
* @param {*} value The value to set.
* @returns {Object} Returns the list cache instance.
*/
function listCacheSet(key, value) {
var data = this.__data__,
index = assocIndexOf(data, key);
if (index < 0) {
++this.size;
data.push([key, value]);
} else {
data[index][1] = value;
}
return this;
}
// Add methods to `ListCache`.
ListCache.prototype.clear = listCacheClear;
ListCache.prototype["delete"] = listCacheDelete;
ListCache.prototype.get = listCacheGet;
ListCache.prototype.has = listCacheHas;
ListCache.prototype.set = listCacheSet;
实现思路
ListCache 是一个基于数组的简单键值对缓存实现,它的核心是使用数组存储键值对,每个键值对以 [key, value]
的形式存储在数组中。它主要提供以下操作:
- 构造函数:初始化缓存并可选地填充初始数据
- 清空操作:移除所有键值对
- 删除操作:移除指定键的键值对
- 获取操作:获取指定键的值
- 检查操作:检查指定键是否存在
- 设置操作:设置或更新指定键的值
所有这些操作依赖于 assocIndexOf
函数来查找键在数组中的位置,而 assocIndexOf
则使用 eq
函数进行键的比较。
源码解析
构造函数
javascript
function ListCache(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
参数,这是一个键值对数组 [[key1, value1], [key2, value2], ...]
。构造过程如下:
- 首先调用
this.clear()
初始化内部存储结构 - 如果提供了
entries
,则遍历它,并依次调用this.set()
设置每一个键值对
例如:
javascript
// 创建一个空的 ListCache
const emptyCache = new ListCache();
// 内部结构: __data__ = [], size = 0
// 创建一个带有初始数据的 ListCache
const cache = new ListCache([
["key1", "value1"],
["key2", "value2"],
]);
// 内部结构: __data__ = [['key1', 'value1'], ['key2', 'value2']], size = 2
clear 方法
javascript
function listCacheClear() {
this.__data__ = [];
this.size = 0;
}
清空方法非常简单,它将内部数据重置为空数组,并将 size 设为 0。
例如:
javascript
cache.clear();
// 执行后: __data__ = [], size = 0
delete 方法
javascript
function listCacheDelete(key) {
var data = this.__data__,
index = assocIndexOf(data, key);
if (index < 0) {
return false;
}
var lastIndex = data.length - 1;
if (index == lastIndex) {
data.pop();
} else {
splice.call(data, index, 1);
}
--this.size;
return true;
}
删除方法根据键删除对应的键值对:
- 首先,使用
assocIndexOf
查找键在数组中的索引 - 如果没找到(索引为 -1),直接返回 false 表示删除失败
- 如果找到了且是数组的最后一个元素,使用
pop()
快速删除 - 如果找到了但不是最后一个元素,使用
splice()
从数组中删除该元素 - 最后,将 size 减 1 并返回 true 表示删除成功
这里有一个性能优化:当删除的是最后一个元素时,使用更高效的 pop()
而不是 splice()
。
例如:
javascript
// 假设 cache.__data__ = [['key1', 'value1'], ['key2', 'value2']]
const result = cache.delete("key1");
// 执行后:__data__ = [['key2', 'value2']], size = 1, result = true
const notFound = cache.delete("key3");
// 执行后:__data__ 不变,size 不变,notFound = false
get 方法
javascript
function listCacheGet(key) {
var data = this.__data__,
index = assocIndexOf(data, key);
return index < 0 ? undefined : data[index][1];
}
获取方法根据键获取对应的值:
- 使用
assocIndexOf
查找键在数组中的索引 - 如果没找到,返回 undefined
- 如果找到了,返回对应键值对的值(即
data[index][1]
)
例如:
javascript
// 假设 cache.__data__ = [['key1', 'value1'], ['key2', 'value2']]
const value = cache.get("key1");
// value = 'value1'
const missingValue = cache.get("key3");
// missingValue = undefined
has 方法
javascript
function listCacheHas(key) {
return assocIndexOf(this.__data__, key) > -1;
}
检查方法判断指定的键是否存在:
- 使用
assocIndexOf
查找键在数组中的索引 - 如果索引大于 -1,说明找到了,返回 true;否则返回 false
例如:
javascript
// 假设 cache.__data__ = [['key1', 'value1'], ['key2', 'value2']]
const hasKey = cache.has("key2");
// hasKey = true
const doesNotHaveKey = cache.has("key3");
// doesNotHaveKey = false
set 方法
javascript
function listCacheSet(key, value) {
var data = this.__data__,
index = assocIndexOf(data, key);
if (index < 0) {
++this.size;
data.push([key, value]);
} else {
data[index][1] = value;
}
return this;
}
设置方法用于添加新键值对或更新已有键值对:
- 使用
assocIndexOf
查找键在数组中的索引 - 如果没找到(索引为 -1):
- 将 size 加 1
- 将新的键值对
[key, value]
添加到数组末尾
- 如果找到了(索引不为 -1):
- 更新已存在键值对的值(
data[index][1]
)
- 更新已存在键值对的值(
- 返回
this
以支持链式调用
例如:
javascript
// 假设 cache.__data__ = [['key1', 'value1']], size = 1
cache.set("key2", "value2");
// 执行后:__data__ = [['key1', 'value1'], ['key2', 'value2']], size = 2
cache.set("key1", "new value");
// 执行后:__data__ = [['key1', 'new value'], ['key2', 'value2']], size = 2
应用场景
ListCache 在 Lodash 中主要用作其他缓存结构的基础构建块,特别是:
- Stack 缓存的默认实现:当数据量小时,Stack 使用 ListCache 存储数据
- MapCache 的回退实现:当环境不支持 Map 时,部分 MapCache 功能通过 ListCache 实现
- 小数据量的键值存储:适用于数据量较小的场景
实际应用示例:
javascript
// 实现一个简单的键值对存储
function createCache() {
return new ListCache();
}
const cache = createCache();
cache.set("userId", 12345);
cache.set("username", "john_doe");
if (cache.has("userId")) {
console.log("User ID:", cache.get("userId"));
}
// 清除特定用户信息
cache.delete("userId");
总结
ListCache 是一个基于数组的简单键值对缓存实现,它通过巧妙的数组操作提供了高效的键值对存储。其设计体现了以下几个特点:
- 简单高效:使用原生数组实现,结构简单,适合小数据量场景
- 完整接口:提供了完整的 CRUD 操作(增、删、改、查)
- 性能优化:在删除操作中针对不同情况选择最优算法
从设计原则角度看,ListCache 体现了:
- 单一职责原则:专注于提供键值对缓存功能
- 接口一致性:与其他缓存实现(如 MapCache)保持一致的接口
- 实现隐藏:用户不需要关心内部如何存储和查找数据
这种实现方式对我们有以下启示:
- 在数据量较小的场景下,简单的数组操作可能比复杂的数据结构更高效
- 设计数据结构时,应根据使用场景选择合适的底层实现
- 即使是简单的数据结构,也可以通过精心设计提供强大而一致的接口