← 中文资源

简体中文版本

带 TTL 的函数记忆化缓存

按自定义键缓存函数结果,支持过期时间、容量上限、淘汰统计与手动清空。

阅读英文原文

功能说明

memoize 把函数包装成带缓存的版本,默认用 JSON.stringify 序列化参数作为键,并为每个条目设置存活时间。命中未过期的条目时直接返回缓存值,并把该条目移到 Map 的末尾;过期条目会被删除并计入 expirations。缓存超过 maxSize 时淘汰最早插入的键,因此它同时具备简单的容量保护。

返回的包装函数附带 clear() 和 stats(),方便测试和运行时观测。now 可以注入自定义时钟,从而在测试中精确控制 TTL,而不必真的等待。所有参数校验都在包装时完成,避免在热路径上反复检查配置。

关键代码

命中逻辑先比较 expiresAt 与当前时间;未过期时删除再重新插入,以刷新 Map 的插入顺序。未命中则调用原函数,并在写入前检查容量。

const existing = cache.get(cacheKey);
if (existing !== undefined) {
  if (existing.expiresAt > timestamp) {
    stats.hits += 1;
    cache.delete(cacheKey);
    cache.set(cacheKey, existing);
    return existing.value;
  }
  cache.delete(cacheKey);
  stats.expirations += 1;
}

stats() 返回命中、未命中、淘汰、过期和当前大小的快照,不会暴露内部 Map。

使用注意

该实现缓存的是函数的返回值,不会自动等待 Promise。如果被包装函数是异步的,缓存中会保存 Promise,拒绝的 Promise 也会被缓存直到过期,调用方需要自行处理或在失败时清空。默认键依赖 JSON 序列化,undefined、函数、Symbol 以及属性顺序可能造成键冲突,生产环境建议传入稳定的自定义键函数。

TTL 从调用开始时计算,而不是从异步操作完成时计算;对耗时接近 TTL 的函数要留出余量。maxSize 淘汰的是最早插入的键,而不是最少使用的键,若需要 LRU 语义应改用专门实现。clear() 只清空缓存,不重置统计计数。若 key 或 now 抛错,异常会直接传播。

验证方式

示例对平方函数设置 40 毫秒 TTL:连续两次调用 slowSquare(4) 只执行一次,第三次调用不同参数后 calls 为 2,统计显示一次命中、两次未命中。等待 50 毫秒后再次调用,calls 变为 3,expirations 增加。调用 clear() 后 size 归零,可确认手动失效生效。