Lodash源码阅读-Stack

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 数据结构的核心思想是通过自动切换底层实现来保持高效性能。它使用两种不同的缓存机制:

  1. 对于少量数据(数组长度 < 199),使用基于数组的 ListCache
  2. 当数据量增大时(数组长度 >= 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 类中最复杂的方法,也是性能优化的核心。流程如下:

  1. 检查当前存储结构是否为 ListCache
  2. 如果是 ListCache,则检查:
    • Map 构造函数是否不存在,或者
    • 当前数据数量是否小于 LARGE_ARRAY_SIZE - 1 (199)
  3. 如果条件满足,则将新键值对直接添加到数组中
  4. 如果条件不满足(数据量达到阈值),则创建一个新的 MapCache,并将数据迁移过去
  5. 无论当前存储结构是什么,最后都调用其 set 方法设置键值对
  6. 同步 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 内部主要用于:

  1. 深拷贝和克隆操作:在处理循环引用时维护已访问对象
  2. 相等性比较:在比较对象时缓存已比较过的对象对
  3. 去重操作:在 uniq、union 等函数中缓存已见过的值

总结

Stack 是 Lodash 内部使用的高效缓存结构,其核心设计思想是根据数据量大小自动切换底层实现。这种设计体现了以下软件设计原则:

  1. 适应性策略模式:根据数据规模动态选择最佳的数据结构实现
  2. 封装变化:对外提供统一接口,内部实现可以灵活变化
  3. 性能优化:小数据量用数组保持简单高效,大数据量用 Map 保持查找性能

这种实现方式对我们有以下启示:

  1. 在构建数据结构时,可以综合考虑不同数据规模下的性能特点
  2. 通过对内部实现的抽象和封装,可以在不影响外部接口的情况下优化性能
  3. 充分利用 JavaScript 原生对象(如 Map)的特性来提升性能
相关推荐
Nicholas681 分钟前
flutterAppBar之SystemUiOverlayStyle源码解析(一)
前端
黑客飓风18 分钟前
JavaScript 性能优化实战大纲
前端·javascript·性能优化
emojiwoo2 小时前
【前端基础知识系列六】React 项目基本框架及常见文件夹作用总结(图文版)
前端·react.js·前端框架
张人玉2 小时前
XML 序列化与操作详解笔记
xml·前端·笔记
杨荧2 小时前
基于Python的宠物服务管理系统 Python+Django+Vue.js
大数据·前端·vue.js·爬虫·python·信息可视化
YeeWang3 小时前
🎉 Eficy 让你的 Cherry Studio 直接生成可预览的 React 页面
前端·javascript
gnip3 小时前
Jenkins部署前端项目实战方案
前端·javascript·架构
Orange3015113 小时前
《深入源码理解webpack构建流程》
前端·javascript·webpack·typescript·node.js·es6
lovepenny4 小时前
Failed to resolve entry for package "js-demo-tools". The package may have ......
前端·npm
超凌4 小时前
threejs 创建了10w条THREE.Line,销毁数据,等待了10秒
前端