Lodash 源码阅读-Stack
概述
Stack 是 Lodash 内部使用的栈结构实现,主要用于缓存键值对数据。它有一个特殊的性能优化策略:当数据量较小时使用简单的数组存储,当数据量增大时自动切换到 Map 结构存储,以提高大数据量下的性能。
前置学习
- ListCache:用于小数据量存储的简单键值对集合,基于数组实现
- MapCache:用于大数据量存储的键值对集合,基于 Map 实现
- getNative:用于获取原生 JavaScript 对象(如 Map)
技术知识:
- JavaScript 的数据结构:数组和 Map
- JavaScript 中的原型继承
- 性能优化策略
源码实现
javascript
/**
* Creates a stack cache object to store key-value pairs.
*
* @private
* @constructor
* @param {Array} [entries] The key-value pairs to cache.
*/
function Stack(entries) {
var data = (this.__data__ = new ListCache(entries));
this.size = data.size;
}
/**
* Removes all key-value entries from the stack.
*
* @private
* @name clear
* @memberOf Stack
*/
function stackClear() {
this.__data__ = new ListCache();
this.size = 0;
}
/**
* Removes `key` and its value from the stack.
*
* @private
* @name delete
* @memberOf Stack
* @param {string} key The key of the value to remove.
* @returns {boolean} Returns `true` if the entry was removed, else `false`.
*/
function stackDelete(key) {
var data = this.__data__,
result = data["delete"](key);
this.size = data.size;
return result;
}
/**
* Gets the stack value for `key`.
*
* @private
* @name get
* @memberOf Stack
* @param {string} key The key of the value to get.
* @returns {*} Returns the entry value.
*/
function stackGet(key) {
return this.__data__.get(key);
}
/**
* Checks if a stack value for `key` exists.
*
* @private
* @name has
* @memberOf Stack
* @param {string} key The key of the entry to check.
* @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
*/
function stackHas(key) {
return this.__data__.has(key);
}
/**
* Sets the stack `key` to `value`.
*
* @private
* @name set
* @memberOf Stack
* @param {string} key The key of the value to set.
* @param {*} value The value to set.
* @returns {Object} Returns the stack cache instance.
*/
function stackSet(key, value) {
var data = this.__data__;
if (data instanceof ListCache) {
var pairs = data.__data__;
if (!Map || pairs.length < LARGE_ARRAY_SIZE - 1) {
pairs.push([key, value]);
this.size = ++data.size;
return this;
}
data = this.__data__ = new MapCache(pairs);
}
data.set(key, value);
this.size = data.size;
return this;
}
// Add methods to `Stack`.
Stack.prototype.clear = stackClear;
Stack.prototype["delete"] = stackDelete;
Stack.prototype.get = stackGet;
Stack.prototype.has = stackHas;
Stack.prototype.set = stackSet;
实现思路
Stack 数据结构的核心思想是通过自动切换底层实现来保持高效性能。它使用两种不同的缓存机制:
- 对于少量数据(数组长度 < 199),使用基于数组的 ListCache
- 当数据量增大时(数组长度 >= 199),自动切换到基于 Map 的 MapCache
这种策略结合了数组在小数据量时的简单高效和 Map 在大数据量时的查找性能优势。Stack 类提供了标准的增删改查接口(clear、delete、get、has、set),通过原型方法实现。
源码解析
构造函数
javascript
function Stack(entries) {
var data = (this.__data__ = new ListCache(entries));
this.size = data.size;
}
构造函数接收一个可选的 entries 参数,这是一个键值对数组。它创建了一个 ListCache 实例用于初始数据存储,然后将其赋值给 __data__
属性,并同步 size 属性。
例如:
javascript
const stack = new Stack([
["key1", "value1"],
["key2", "value2"],
]);
// 内部结构:
// stack.__data__.__data__ = [['key1', 'value1'], ['key2', 'value2']]
// stack.size = 2
clear 方法
javascript
function stackClear() {
this.__data__ = new ListCache();
this.size = 0;
}
清空栈的所有数据,重置为一个新的 ListCache 实例,并将 size 设为 0。
例如:
javascript
stack.clear();
// 执行后:
// stack.__data__.__data__ = []
// stack.size = 0
delete 方法
javascript
function stackDelete(key) {
var data = this.__data__,
result = data["delete"](key);
this.size = data.size;
return result;
}
删除指定键的数据,并返回删除操作是否成功。内部委托给 __data__
的 delete 方法,然后同步 size 属性。
例如:
javascript
const result = stack.delete("key1");
// 如果key1存在,则result为true,并且size减1
// 如果key1不存在,则result为false,size不变
get 方法
javascript
function stackGet(key) {
return this.__data__.get(key);
}
获取指定键的值,内部直接委托给 __data__
的 get 方法。
例如:
javascript
const value = stack.get("key2");
// 如果key2存在,则返回对应的值
// 如果key2不存在,则返回undefined
has 方法
javascript
function stackHas(key) {
return this.__data__.has(key);
}
检查是否存在指定键,内部直接委托给 __data__
的 has 方法。
例如:
javascript
const exists = stack.has("key2");
// 如果key2存在,则返回true
// 如果key2不存在,则返回false
set 方法
javascript
function stackSet(key, value) {
var data = this.__data__;
if (data instanceof ListCache) {
var pairs = data.__data__;
if (!Map || pairs.length < LARGE_ARRAY_SIZE - 1) {
pairs.push([key, value]);
this.size = ++data.size;
return this;
}
data = this.__data__ = new MapCache(pairs);
}
data.set(key, value);
this.size = data.size;
return this;
}
这是 Stack 类中最复杂的方法,也是性能优化的核心。流程如下:
- 检查当前存储结构是否为 ListCache
- 如果是 ListCache,则检查:
- Map 构造函数是否不存在,或者
- 当前数据数量是否小于 LARGE_ARRAY_SIZE - 1 (199)
- 如果条件满足,则将新键值对直接添加到数组中
- 如果条件不满足(数据量达到阈值),则创建一个新的 MapCache,并将数据迁移过去
- 无论当前存储结构是什么,最后都调用其 set 方法设置键值对
- 同步 size 属性并返回实例本身,支持链式调用
例如:
javascript
// 当数据量小时
stack.set("key3", "value3");
// 直接添加到数组:stack.__data__.__data__ = [...原数据, ['key3', 'value3']]
// 当数据量接近200时
stack.set("key200", "value200");
// 可能触发从ListCache到MapCache的转换
// 转换后:stack.__data__ 变成 MapCache 实例
原型方法绑定
javascript
Stack.prototype.clear = stackClear;
Stack.prototype["delete"] = stackDelete;
Stack.prototype.get = stackGet;
Stack.prototype.has = stackHas;
Stack.prototype.set = stackSet;
这部分将各个操作函数绑定到 Stack 的原型上。注意 delete 方法使用方括号语法 ['delete']
而不是点语法,这是因为 delete 是 JavaScript 的保留字。
应用场景
Stack 在 Lodash 内部主要用于:
- 深拷贝和克隆操作:在处理循环引用时维护已访问对象
- 相等性比较:在比较对象时缓存已比较过的对象对
- 去重操作:在 uniq、union 等函数中缓存已见过的值
总结
Stack 是 Lodash 内部使用的高效缓存结构,其核心设计思想是根据数据量大小自动切换底层实现。这种设计体现了以下软件设计原则:
- 适应性策略模式:根据数据规模动态选择最佳的数据结构实现
- 封装变化:对外提供统一接口,内部实现可以灵活变化
- 性能优化:小数据量用数组保持简单高效,大数据量用 Map 保持查找性能
这种实现方式对我们有以下启示:
- 在构建数据结构时,可以综合考虑不同数据规模下的性能特点
- 通过对内部实现的抽象和封装,可以在不影响外部接口的情况下优化性能
- 充分利用 JavaScript 原生对象(如 Map)的特性来提升性能