Vanilla Lru 设计原理
为什么 extends Map
本项目的目标之一是提供原生的 Map 风格 API。继承自 Map 的类是满足该契约最直接的方式:
cache instanceof Map返回 true。size、迭代操作、keys()、values()、entries()和forEach()的行为符合用户对Map的预期。set()可以返回this,从而支持正常的Map链式调用。
虽然可以使用函数式工厂模式:
JavaScriptfunction createLru(options) { return new Lru(options);}
但纯函数式实现无法真正成为一个原生的 Map 子类。它要么返回一个具有类似方法的普通对象,要么使用 Proxy,这会增加复杂性并削弱兼容性。
对于本包而言,class Lru extends Map 是更好的默认选择。
为什么使用两个 Map
经典的 LRU 缓存通常维护一个链表加上一个哈希表。这种方式虽然精确,但需要额外的节点和指针更新开销。双 Map 架构以牺牲完美的逐条目排序为代价,换取更简单且非常快速的实现:
- 新的写入操作进入"最近"(recent)Map。
- 从"旧"(old)Map 中读取条目时,将该条目移动到"最近" Map。
- 当"最近" Map 达到
maxSize时,“旧” Map 被驱逐(清空),而"最近" Map 变为"旧" Map。
其结果是实现了近似 LRU 的行为,具有出色的实际性能且代码量极少。
过期模型
条目存储一个绝对的 expiry(过期)时间戳。当通过 get()、has()、peek()、写入侧轮换或迭代操作访问条目时,过期的条目会被惰性移除。expiresIn() 特意设计为只读操作:它报告剩余时间,但不会改变最近使用状态或移除过期条目。
size getter 同样保持无副作用。它报告已存储的条目数量,因此在惰性清理发生前可能包含已经过期的条目。
这种方式避免了后台定时器,保持包的确定性,并且适用于可能被节流或挂起的浏览器标签页。
驱逐钩子(Eviction hook)
当条目因为容量轮换、手动调用 evict() 或惰性过期清理而被移除时,会调用 onEviction 回调。显式调用 delete() 或 clear() 不会触发这个回调。