WeakRef + LRU 组合不可用 Map + lru_cache 替代,因后者强引用阻止 GC 回收,而 WeakMap 仅支持对象键、需配合 Map 元数据与 FinalizationRegistry 实现弱引用 LRU 缓存。

WeakRef + LRU 的组合为什么不能直接用 Map + lru_cache?
因为 lru_cache 本质是强引用缓存,它把函数返回值牢牢锁在内存里,哪怕调用方早已丢弃该结果,lru_cache 仍会阻止 GC 回收——这在长期运行的智能体或 Web 服务中极易引发内存堆积。而原生 Map 存储的也是强引用,无法感知值对象是否已被外部释放。
真正需要的是:缓存键可查、值生命周期由 GC 自主决定、淘汰策略仍按访问频次/时序控制。这必须拆解实现,不能靠装饰器一揽子解决。
用 WeakMap + 手动 LRU 链表模拟弱引用 LRU 缓存
JavaScript 中没有“弱引用 + LRU”原生类型,但可用 WeakMap 存值 + 单独维护一个带时间戳和访问计数的键元数据结构来逼近效果。关键约束是:WeakMap 只支持对象作键,不支持字符串或数字键,所以需封装一层 key 映射逻辑。
- 用
Map存键元信息(key → { lastAccess, hitCount, valueRef }),其中valueRef是WeakRef实例 - 每次
get(key)时先检查valueRef.deref()是否有效;无效则清除元数据项 - 每次访问后更新
lastAccess和hitCount,并重排 LRU 顺序(如用数组索引或Set维护访问序列) -
set(key, value)前先判断是否超限,若超限则从 LRU 序列尾部开始逐个deref()检查,跳过已失效项,直到腾出空间
注意:WeakMap 在这里仅用于辅助验证对象存活,实际缓存主体仍是那个 Map,只是它的值字段被弱化了。
FinalizationRegistry 如何补全自动清理闭环?
仅靠 deref() 检查不够:如果某个缓存值被外部强引用持有,但长期未被访问,它仍会滞留在元数据结构中,导致键表膨胀。这时要靠 FinalizationRegistry 主动通知“这个值确实死了”。
- 每次
set()时,对value调用registry.register(value, key, weakRef) - 回调中只做一件事:
metadataMap.delete(key),不操作WeakRef或尝试deref() - 必须传入
weakRef作为 unregister token,否则后续无法精准注销(避免重复触发) - 不要在回调里执行复杂逻辑或异步操作——FinalizationRegistry 回调运行在不可预测时机,且无执行保证
这个注册动作本身不增加引用,但能让缓存元数据与对象生命周期真正对齐。
为什么不用 WeakRef 直接存整个键值对?
因为 WeakRef 只能包装单个对象,不能包装键+值的组合。如果你写 new WeakRef({ key, value }),那整个对象成为弱引用目标,一旦没人强引用这个包装对象,键和值同时消失——你失去了按 key 查找的能力。
更危险的是:若多个缓存项共用同一个包装对象(比如复用对象字面量),GC 回收会误删其他项。所以必须分离存储:键用强引用(Map 管理),值用 WeakRef 封装,生命周期解耦。
实际部署时,建议给缓存加 maxSize 和 onEvict 钩子,前者控规模,后者在强制淘汰前做资源释放(如关闭未完成的 AbortController 或取消 pending fetch)。WeakRef 不负责“主动淘汰”,只负责“被动善后”。










