Lodash 源码阅读-memoize
概述
memoize
函数是 Lodash 中实现函数记忆化的工具,它能将函数调用的结果缓存起来。当你用相同参数多次调用一个函数时,只有第一次会真正执行计算,后续调用直接返回缓存结果,大大提升了性能。特别适合处理计算密集型操作或重复调用频繁的场景。
前置学习
依赖函数
MapCache
:Lodash 内部实现的缓存数据结构,提供 get、set、has 等方法,用于存储函数调用结果
技术知识
- 函数记忆化:通过缓存结果优化函数性能的技术
- 闭包:JavaScript 中函数访问其外部作用域变量的特性
- 高阶函数:接收或返回函数的函数
- this 绑定:JavaScript 中函数执行上下文的传递
- 缓存键生成:如何为不同的函数调用生成唯一标识
源码实现
js
/**
* Creates a function that memoizes the result of `func`. If `resolver` is
* provided, it determines the cache key for storing the result based on the
* arguments provided to the memoized function. By default, the first argument
* provided to the memoized function is used as the map cache key. The `func`
* is invoked with the `this` binding of the memoized function.
*
* **Note:** The cache is exposed as the `cache` property on the memoized
* function. Its creation may be customized by replacing the `_.memoize.Cache`
* constructor with one whose instances implement the
* [`Map`](http://ecma-international.org/ecma-262/7.0/#sec-properties-of-the-map-prototype-object)
* method interface of `clear`, `delete`, `get`, `has`, and `set`.
*
* @static
* @memberOf _
* @since 0.1.0
* @category Function
* @param {Function} func The function to have its output memoized.
* @param {Function} [resolver] The function to resolve the cache key.
* @returns {Function} Returns the new memoized function.
* @example
*
* var object = { 'a': 1, 'b': 2 };
* var other = { 'c': 3, 'd': 4 };
*
* var values = _.memoize(_.values);
* values(object);
* // => [1, 2]
*
* values(other);
* // => [3, 4]
*
* object.a = 2;
* values(object);
* // => [1, 2]
*
* // Modify the result cache.
* values.cache.set(object, ['a', 'b']);
* values(object);
* // => ['a', 'b']
*
* // Replace `_.memoize.Cache`.
* _.memoize.Cache = WeakMap;
*/
function memoize(func, resolver) {
if (
typeof func != "function" ||
(resolver != null && typeof resolver != "function")
) {
throw new TypeError(FUNC_ERROR_TEXT);
}
var memoized = function () {
var args = arguments,
key = resolver ? resolver.apply(this, args) : args[0],
cache = memoized.cache;
if (cache.has(key)) {
return cache.get(key);
}
var result = func.apply(this, args);
memoized.cache = cache.set(key, result) || cache;
return result;
};
memoized.cache = new (memoize.Cache || MapCache)();
return memoized;
}
// Expose `MapCache`.
memoize.Cache = MapCache;
实现思路
memoize
函数的实现思路很直接:先检查输入参数的合法性,然后创建并返回一个新函数 memoized
。这个新函数会在调用时自动检查传入的参数是否已有缓存结果,有则直接返回缓存值,没有则执行原函数并将结果存入缓存。同时,它还暴露了缓存对象,允许用户自定义缓存实现或直接操作缓存内容。整个设计既简洁又灵活,可以适应多种缓存需求。
源码解析
参数验证
js
if (
typeof func != "function" ||
(resolver != null && typeof resolver != "function")
) {
throw new TypeError(FUNC_ERROR_TEXT);
}
这段代码验证输入参数是否合法:
func
必须是函数类型- 如果提供了
resolver
,它也必须是函数类型
不满足条件就抛出类型错误。这种严格的参数检查能够及早发现问题,避免在后续执行中出现难以预料的错误。
示例:
js
_.memoize("not a function"); // 抛出 TypeError
_.memoize(function () {}, "not a function"); // 抛出 TypeError
创建记忆化函数
js
var memoized = function () {
var args = arguments,
key = resolver ? resolver.apply(this, args) : args[0],
cache = memoized.cache;
if (cache.has(key)) {
return cache.get(key);
}
var result = func.apply(this, args);
memoized.cache = cache.set(key, result) || cache;
return result;
};
这是 memoize
的核心逻辑,创建一个新函数处理缓存逻辑:
-
生成缓存键:
jskey = resolver ? resolver.apply(this, args) : args[0];
- 如果提供了
resolver
函数,使用它来生成键(传入所有参数) - 否则默认使用第一个参数
args[0]
作为键(传入的参数)
示例:
js// 使用自定义键生成器处理多参数 const add = _.memoize( (a, b) => a + b, (a, b) => `${a}-${b}` // 生成唯一键 ); add(1, 2); // 计算 3 并缓存 add(1, 2); // 直接返回缓存的 3
- 如果提供了
-
检查缓存:
jsif (cache.has(key)) { return cache.get(key); }
如果缓存中有对应的键,直接返回缓存结果,避免重复计算。
-
执行函数和缓存结果:
jsvar result = func.apply(this, args); memoized.cache = cache.set(key, result) || cache; return result;
- 使用
apply
确保原函数运行时的this
上下文正确 - 缓存计算结果并返回
这里的
cache.set(key, result) || cache
是个技巧,因为某些缓存实现的set
方法可能不返回自身实例,用||
确保总是返回缓存对象。 - 使用
初始化缓存
js
memoized.cache = new (memoize.Cache || MapCache)();
return memoized;
这行代码初始化缓存并挂载到函数上:
- 使用
memoize.Cache
构造函数(如已自定义)或默认的MapCache
- 将缓存实例挂载到
memoized.cache
属性,方便外部访问和操作
暴露缓存实现
js
// Expose `MapCache`.
memoize.Cache = MapCache;
暴露默认缓存构造函数为 memoize.Cache
,这样用户可以替换成自己的缓存实现:
js
// 自定义缓存实现示例
_.memoize.Cache = WeakMap; // 使用 WeakMap 允许键被垃圾回收
只要替代的缓存类实现了 get
、set
、has
方法接口即可。
总结
memoize
函数是一个简洁但功能强大的工具,它通过缓存计算结果显著提升了函数性能。它的设计有几个亮点:
- 简单接口与灵活性平衡:仅需两个参数就能实现强大的记忆化功能,同时保留了高度定制能力
- 良好的关注点分离:缓存逻辑与原函数逻辑完全分离,符合单一职责原则
- 开放/封闭原则实践 :通过暴露
memoize.Cache
,允许扩展但不需修改源码 - 优雅的缓存键设计:默认使用第一个参数作为键,同时支持自定义键生成策略
- 保留函数上下文 :正确传递
this
绑定,确保函数行为一致性
这些设计使 memoize
成为优化性能的强大工具,特别适合计算密集型函数、API 请求缓存和任何需要避免重复计算的场景。