Lodash源码阅读-memoizeCapped

Lodash 源码阅读-memoizeCapped

概述

memoizeCapped 是 Lodash 内部的一个特殊函数,用来创建带缓存上限的记忆化函数。它给普通的记忆化函数加了个"安全阀"------当缓存数量达到上限(默认 500 个)时,会自动清空所有缓存。这个函数主要用在 Lodash 内部,特别是处理那些可能有无限多输入的场景,防止内存无限增长导致泄漏。

前置学习

依赖函数

  • memoize:Lodash 的基础记忆化函数,memoizeCapped 是在它基础上的封装
  • MapCache:Lodash 内部缓存数据结构,提供 getsethasclear 等方法

技术知识

  • 函数记忆化:通过缓存函数结果提高性能的技术
  • 闭包:利用作用域链访问外部变量的机制
  • 缓存策略:特别是缓存大小限制和清除策略
  • 变量提升和函数声明提升:JavaScript 的特性,影响变量和函数的访问顺序

源码实现

js 复制代码
/**
 * A specialized version of `_.memoize` which clears the memoized function's
 * cache when it exceeds `MAX_MEMOIZE_SIZE`.
 *
 * @private
 * @param {Function} func The function to have its output memoized.
 * @returns {Function} Returns the new memoized function.
 */
function memoizeCapped(func) {
  var result = memoize(func, function (key) {
    if (cache.size === MAX_MEMOIZE_SIZE) {
      cache.clear();
    }
    return key;
  });

  var cache = result.cache;
  return result;
}

实现思路

memoizeCapped 的实现非常巧妙:它调用 memoize 创建一个记忆化函数,但提供了自定义的解析器函数作为第二个参数。这个解析器函数会在每次生成缓存键前检查缓存大小,如果达到上限(MAX_MEMOIZE_SIZE,默认是 500),就立即清空整个缓存。然后,它把解析器收到的原始键值原样返回作为缓存键。有趣的是,代码先定义了引用缓存的变量 cache,然后才真正获取到这个缓存对象------这利用了 JavaScript 的闭包和变量声明提升特性。

源码解析

函数声明和参数

js 复制代码
function memoizeCapped(func) {
  // ...
}

memoizeCapped 只接收一个参数 func,这是要被记忆化的函数。不同于 memoize,它不允许用户自定义解析器函数,因为内部已经使用了特定的解析器实现缓存上限控制。

示例:

js 复制代码
// 使用方式
var calculateCapped = memoizeCapped(function (input) {
  console.log("计算中...");
  return input * 2;
});

calculateCapped(5); // 打印"计算中..."并返回10
calculateCapped(5); // 直接返回缓存的10,不打印

创建记忆化函数

js 复制代码
var result = memoize(func, function (key) {
  if (cache.size === MAX_MEMOIZE_SIZE) {
    cache.clear();
  }
  return key;
});

这里调用 memoize 函数,传入:

  1. 原始函数 func
  2. 自定义解析器函数

这个解析器函数非常有意思,它做两件事:

  • 检查缓存大小是否已达到 MAX_MEMOIZE_SIZE(默认值是 500)
  • 如果达到上限,则调用 cache.clear() 清空整个缓存

注意这里的 cache 变量在解析器函数定义时还不存在!这段代码依赖 JavaScript 的闭包和变量提升特性 - 解析器函数可以访问稍后才声明的 cache 变量。

解析器函数与键生成的关系

要理解这部分代码,我们需要知道 memoize 是如何生成和使用缓存键的:

js 复制代码
// memoize 内部的键生成逻辑(简化版)
key = resolver ? resolver.apply(this, args) : args[0];

这意味着:

  • 如果提供了解析器函数,memoize 会用它来生成缓存键
  • 如果没有提供,就简单地用第一个参数作为键

memoizeCapped 中:

  1. 我们提供了解析器函数 function(key) { ... return key; }

  2. 但这个解析器接收的参数 key 实际上是什么?

    • 这是 memoize 默认会用作缓存键的值 - 通常是被记忆化函数的第一个参数
    • 也就是说,这里的 key 已经是 args[0](第一个参数)了
  3. 为什么解析器直接返回 key

    • 解析器并不打算"创造"新的键,而是利用了 memoize 调用解析器的时机
    • 它简单地将收到的值原样返回,所以最终使用的缓存键仍然是第一个参数
    • 解析器的主要目的是"顺便"检查缓存大小并可能清除缓存

举个实际例子:

js 复制代码
var pathParser = memoizeCapped(function (path) {
  return path.split(".");
});

// 调用过程
pathParser("a.b.c");
// 1. memoize 准备使用 'a.b.c' 作为键(因为它是第一个参数)
// 2. 但由于提供了解析器,memoize 会把 'a.b.c' 传给解析器
// 3. 解析器检查缓存大小,返回原始值 'a.b.c'
// 4. memoize 使用返回的值 'a.b.c' 作为缓存键

// 后续处理500个不同路径后...
pathParser("x.y.z"); // 假设这是第501个不同路径
// 1. memoize 准备使用 'x.y.z' 作为键
// 2. 把 'x.y.z' 传给解析器
// 3. 解析器发现缓存已满,清空缓存
// 4. 解析器返回原始值 'x.y.z'
// 5. 由于缓存已清空,将重新计算结果

核心要点:解析器函数在 memoizeCapped 中的作用不是为了创建特殊的键,而是"借用"这个时机来控制缓存大小。

获取缓存引用

js 复制代码
var cache = result.cache;

这行代码从记忆化函数中获取缓存对象引用。由于 JavaScript 的闭包特性,前面定义的解析器函数现在可以访问到这个 cache 变量,即使解析器函数是在 cache 变量声明之前定义的。

在实际运行时,解析器函数的执行会发生在 cache 变量赋值之后,因为只有当记忆化函数被调用时,解析器函数才会执行。这是个巧妙的设计:

js 复制代码
// 简化的执行流程演示
calculateCapped(10); // 第一次调用
// 内部执行:
// 1. 在 result.cache 已存在的情况下
// 2. 解析器函数运行,此时 cache 已定义
// 3. 检查 cache.size 是否等于 MAX_MEMOIZE_SIZE
// 4. 返回 key (10) 作为缓存键

返回结果

js 复制代码
return result;

最后,函数返回构建好的带缓存上限的记忆化函数。用户可以像使用普通的 memoize 函数一样使用它,但后台会自动处理缓存大小限制。

总结

memoizeCapped 是一个轻量级但很实用的工具函数,它巧妙地解决了普通记忆化可能导致的内存泄漏问题。几个值得注意的设计亮点:

  1. 有限缓存策略:通过设置最大缓存数量,防止内存无限增长
  2. 简单的清除策略:一旦达到上限,直接清空全部缓存,实现简单高效
  3. 闭包的巧妙运用:利用 JavaScript 闭包特性使解析器函数访问后定义的变量
  4. 组合优于继承 :通过组合 memoize 实现新功能,而不是继承或修改原函数

这种设计适用于那些:

  1. 需要记忆化来提高性能
  2. 输入变体可能非常多
  3. 缓存命中率不是极高

虽然是 Lodash 的内部函数,但这种缓存控制策略值得我们在自己的项目中借鉴,特别是在处理用户输入、解析文本、API 请求缓存等场景中。

相关推荐
今晚吃什么呢?5 分钟前
前端面试题之CSS中的box属性
前端·css
我是大龄程序员8 分钟前
Babel工作理解
前端
《独白》18 分钟前
将图表和表格导出为PDF的功能
javascript·vue.js·ecmascript
CopyLower21 分钟前
提升 Web 性能:使用响应式图片优化体验
前端
南通DXZ23 分钟前
Win7下安装高版本node.js 16.3.0 以及webpack插件的构建
前端·webpack·node.js
什码情况23 分钟前
微服务集成测试 -华为OD机试真题(A卷、JavaScript)
javascript·数据结构·算法·华为od·机试
你的人类朋友1 小时前
浅谈Object.prototype.hasOwnProperty.call(a, b)
javascript·后端·node.js
Mintopia1 小时前
深入理解 Three.js 中的 Mesh:构建 3D 世界的基石
前端·javascript·three.js
打瞌睡de喵1 小时前
JavaScript 空对象检测
javascript
前端太佬1 小时前
暂时性死区(Temporal Dead Zone, TDZ)
前端·javascript·node.js