Lodash源码阅读-memoize

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 的核心逻辑,创建一个新函数处理缓存逻辑:

  1. 生成缓存键

    js 复制代码
    key = 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
  2. 检查缓存

    js 复制代码
    if (cache.has(key)) {
      return cache.get(key);
    }

    如果缓存中有对应的键,直接返回缓存结果,避免重复计算。

  3. 执行函数和缓存结果

    js 复制代码
    var 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 允许键被垃圾回收

只要替代的缓存类实现了 getsethas 方法接口即可。

总结

memoize 函数是一个简洁但功能强大的工具,它通过缓存计算结果显著提升了函数性能。它的设计有几个亮点:

  1. 简单接口与灵活性平衡:仅需两个参数就能实现强大的记忆化功能,同时保留了高度定制能力
  2. 良好的关注点分离:缓存逻辑与原函数逻辑完全分离,符合单一职责原则
  3. 开放/封闭原则实践 :通过暴露 memoize.Cache,允许扩展但不需修改源码
  4. 优雅的缓存键设计:默认使用第一个参数作为键,同时支持自定义键生成策略
  5. 保留函数上下文 :正确传递 this 绑定,确保函数行为一致性

这些设计使 memoize 成为优化性能的强大工具,特别适合计算密集型函数、API 请求缓存和任何需要避免重复计算的场景。

相关推荐
勇气要爆发21 小时前
问:ES5和ES6的区别
前端·ecmascript·es6
永不停歇的蜗牛1 天前
Maven的POM文件相关标签作用
服务器·前端·maven
芳草萋萋鹦鹉洲哦1 天前
【vue/js】文字超长悬停显示的几种方式
前端·javascript·vue.js
HIT_Weston1 天前
47、【Ubuntu】【Gitlab】拉出内网 Web 服务:Nginx 事件驱动分析(一)
前端·ubuntu·gitlab
开发者小天1 天前
React中的 闭包陷阱
前端·javascript·react.js
翔云 OCR API1 天前
承兑汇票识别接口技术解析-开发者接口
开发语言·前端·数据库·人工智能·ocr
涔溪1 天前
Vue3 的核心语法
前端·vue.js·typescript
国服第二切图仔1 天前
Electron for 鸿蒙pc项目实战之tab标签页组件
javascript·electron·harmonyos·鸿蒙pc
G***E3161 天前
前端在移动端中的React Native Web
前端·react native·react.js
云烟飘渺o1 天前
JPA 的脏检查:一次“没 save() 却更新了”的排查记录
前端