Vanilla Lru 设计原理

为什么 extends Map

本项目的目标之一是提供原生的 Map 风格 API。继承自 Map 的类是满足该契约最直接的方式:

  • cache instanceof Map 返回 true。
  • size、迭代操作、keys()values()entries()forEach() 的行为符合用户对 Map 的预期。
  • set() 可以返回 this,从而支持正常的 Map 链式调用。

虽然可以使用函数式工厂模式:

JavaScript
function createLru(options) { return new Lru(options);}

但纯函数式实现无法真正成为一个原生的 Map 子类。它要么返回一个具有类似方法的普通对象,要么使用 Proxy,这会增加复杂性并削弱兼容性。

对于本包而言,class Lru extends Map 是更好的默认选择。

为什么使用两个 Map

经典的 LRU 缓存通常维护一个链表加上一个哈希表。这种方式虽然精确,但需要额外的节点和指针更新开销。双 Map 架构以牺牲完美的逐条目排序为代价,换取更简单且非常快速的实现:

  1. 新的写入操作进入"最近"(recent)Map。
  2. 从"旧"(old)Map 中读取条目时,将该条目移动到"最近" Map。
  3. 当"最近" Map 达到 maxSize 时,“旧” Map 被驱逐(清空),而"最近" Map 变为"旧" Map。

其结果是实现了近似 LRU 的行为,具有出色的实际性能且代码量极少。

过期模型

条目存储一个绝对的 expiry(过期)时间戳。当通过 get()has()peek()、写入侧轮换或迭代操作访问条目时,过期的条目会被惰性移除。expiresIn() 特意设计为只读操作:它报告剩余时间,但不会改变最近使用状态或移除过期条目。

size getter 同样保持无副作用。它报告已存储的条目数量,因此在惰性清理发生前可能包含已经过期的条目。

这种方式避免了后台定时器,保持包的确定性,并且适用于可能被节流或挂起的浏览器标签页。

驱逐钩子(Eviction hook)

当条目因为容量轮换、手动调用 evict() 或惰性过期清理而被移除时,会调用 onEviction 回调。显式调用 delete()clear() 不会触发这个回调。

最后更新于 2026-09-23 14:49:53 UTC+8