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 请求缓存等场景中。

相关推荐
恋猫de小郭2 分钟前
Flutter Widget IDE 预览新进展,开始推进落地发布
android·前端·flutter
jingling5551 小时前
【Vue3 实战】插槽封装与懒加载
前端·javascript·vue.js
Freedom风间6 小时前
前端优秀编码技巧
前端·javascript·代码规范
萌萌哒草头将军7 小时前
🚀🚀🚀 Openapi:全栈开发神器,0代码写后端!
前端·javascript·next.js
萌萌哒草头将军7 小时前
🚀🚀🚀 Prisma 爱之初体验:一款非常棒的 ORM 工具库
前端·javascript·orm
拉不动的猪7 小时前
SDK与API简单对比
前端·javascript·面试
runnerdancer7 小时前
微信小程序蓝牙通信开发之分包传输通信协议开发
前端
BillKu7 小时前
Vue3后代组件多祖先通讯设计方案
开发语言·javascript·ecmascript
山海上的风7 小时前
Vue里面elementUi-aside 和el-main不垂直排列
前端·vue.js·elementui