Lodash 源码阅读-memoizeCapped
概述
memoizeCapped
是 Lodash 内部的一个特殊函数,用来创建带缓存上限的记忆化函数。它给普通的记忆化函数加了个"安全阀"------当缓存数量达到上限(默认 500 个)时,会自动清空所有缓存。这个函数主要用在 Lodash 内部,特别是处理那些可能有无限多输入的场景,防止内存无限增长导致泄漏。
前置学习
依赖函数
memoize
:Lodash 的基础记忆化函数,memoizeCapped
是在它基础上的封装MapCache
:Lodash 内部缓存数据结构,提供get
、set
、has
、clear
等方法
技术知识
- 函数记忆化:通过缓存函数结果提高性能的技术
- 闭包:利用作用域链访问外部变量的机制
- 缓存策略:特别是缓存大小限制和清除策略
- 变量提升和函数声明提升: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
函数,传入:
- 原始函数
func
- 自定义解析器函数
这个解析器函数非常有意思,它做两件事:
- 检查缓存大小是否已达到
MAX_MEMOIZE_SIZE
(默认值是 500) - 如果达到上限,则调用
cache.clear()
清空整个缓存
注意这里的 cache
变量在解析器函数定义时还不存在!这段代码依赖 JavaScript 的闭包和变量提升特性 - 解析器函数可以访问稍后才声明的 cache
变量。
解析器函数与键生成的关系
要理解这部分代码,我们需要知道 memoize
是如何生成和使用缓存键的:
js
// memoize 内部的键生成逻辑(简化版)
key = resolver ? resolver.apply(this, args) : args[0];
这意味着:
- 如果提供了解析器函数,memoize 会用它来生成缓存键
- 如果没有提供,就简单地用第一个参数作为键
在 memoizeCapped
中:
-
我们提供了解析器函数
function(key) { ... return key; }
-
但这个解析器接收的参数
key
实际上是什么?- 这是
memoize
默认会用作缓存键的值 - 通常是被记忆化函数的第一个参数 - 也就是说,这里的
key
已经是args[0]
(第一个参数)了
- 这是
-
为什么解析器直接返回
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
是一个轻量级但很实用的工具函数,它巧妙地解决了普通记忆化可能导致的内存泄漏问题。几个值得注意的设计亮点:
- 有限缓存策略:通过设置最大缓存数量,防止内存无限增长
- 简单的清除策略:一旦达到上限,直接清空全部缓存,实现简单高效
- 闭包的巧妙运用:利用 JavaScript 闭包特性使解析器函数访问后定义的变量
- 组合优于继承 :通过组合
memoize
实现新功能,而不是继承或修改原函数
这种设计适用于那些:
- 需要记忆化来提高性能
- 输入变体可能非常多
- 缓存命中率不是极高
虽然是 Lodash 的内部函数,但这种缓存控制策略值得我们在自己的项目中借鉴,特别是在处理用户输入、解析文本、API 请求缓存等场景中。