JavaScript WeakRef弱引用在缓存场景中的实际应用
在开发高流量或数据密集型的 Web 应用时,缓存是提升性能的关键手段。然而,使用传统的 JavaScript Map 或普通对象构建缓存,往往面临一个棘手问题:内存泄漏。如果不手动清理,缓存的数据会一直占用内存,直到进程崩溃。
JavaScript 提供的 WeakRef(弱引用)和 FinalizationRegistry(终结注册表)正是为了解决这一问题。它们允许创建“不阻止垃圾回收”的引用,让浏览器在内存紧张时自动清理缓存。
第一阶段:理解强引用与弱引用的区别
在编写代码前,必须先理解为什么传统的“强引用”会导致内存问题,以及“弱引用”是如何工作的。
-
分析强引用的弊端
普通变量或Map中的键值对都是“强引用”。只要一个对象被强引用持有,垃圾回收器(GC)就绝对不敢回收它,即使你在代码中不再需要这个对象。 -
认识弱引用的特性
WeakRef创建的是对目标对象的“弱引用”。这意味着,如果对象没有被其他强引用持有(例如,业务逻辑中的变量已经指向了别处),垃圾回收器就有权在任意时刻回收该对象。此时,通过WeakRef访问对象可能会返回undefined。 -
明确适用场景
弱引用非常适合用于缓存。缓存的特点是“有了更好,没有也能重新计算或获取”。使用弱引用,你可以放心地把数据存起来,而不必担心因为忘记清理而导致内存溢出。
第二阶段:构建基础弱引用缓存
下面我们创建一个简单的缓存类,使用 WeakRef 来存储计算结果,防止内存无限增长。
-
定义缓存类结构
创建一个名为WeakCache的类。它内部维护一个普通的Map,但Map中存储的不是数据本身,而是指向数据的WeakRef对象。 -
实现
set方法
编写方法将数据存入缓存。创建 一个WeakRef实例包裹目标数据,然后将其存入Map。class WeakCache { constructor() { this.cache = new Map(); } set(key, value) { // 使用 WeakRef 包裹 value,建立弱引用 this.cache.set(key, new WeakRef(value)); } get(key) { const ref = this.cache.get(key); if (ref) { // 尝试通过 deref 获取原始对象 const value = ref.deref(); if (value !== undefined) { return value; } else { // 如果对象已被回收,清理 Map 中的 Key this.cache.delete(key); } } return undefined; } } -
实现
get方法
编写读取逻辑。调用ref.deref()方法解引用。检查 返回值:- 如果返回对象,说明缓存命中,直接返回。
- 如果返回
undefined,说明原对象已被垃圾回收。执行this.cache.delete(key)清理无效的键值对,并返回undefined。
第三阶段:自动化清理缓存键(进阶优化)
上述代码虽然解除了对数据的强引用,但 Map 中的 key(通常是字符串或数字)和 WeakRef 包装对象本身依然占据内存。当数据被回收后,Map 里会留下大量空的“壳”。我们需要引入 FinalizationRegistry 来自动清理这些“壳”。
-
理解 FinalizationRegistry 的作用
FinalizationRegistry允许你注册一个对象,并指定一个“回调函数”。当该对象被垃圾回收时,系统会异步调用这个回调函数。我们可以利用这个回调来清理Map中的 Key。 -
在类中集成注册表
修改WeakCache类的构造函数,初始化 一个FinalizationRegistry实例。class AutoCleanWeakCache { constructor() { this.cache = new Map(); // 初始化注册表,定义回调函数:被回收对象的 key 将被传入 this.registry = new FinalizationRegistry((heldValue) => { // heldValue 是我们在注册时传入的 key this.cache.delete(heldValue); console.log(`Key [${heldValue}] 已自动清理`); }); } set(key, value) { // 1. 将 WeakRef 存入 Map this.cache.set(key, new WeakRef(value)); // 2. 将 value 对象注册到注册表 // 参数:目标对象,注销时要保留的数据,注销时需要关联的 Token this.registry.register(value, key, key); } get(key) { const ref = this.cache.get(key); if (!ref) return undefined; const value = ref.deref(); if (value === undefined) { this.cache.delete(key); return undefined; } return value; } } -
注册对象与回调逻辑
在set方法中,调用this.registry.register(value, key, key)。- 第一个参数是我们要监视的对象(即缓存的数据)。
- 第二个参数是“保留值”,即
key。当对象被回收时,回调函数会收到这个key,从而知道该删除 Map 中的哪一项。 - 第三个参数是“注销令牌”,这里直接复用
key,以便后续如果需要手动注销时使用。
第四阶段:验证与测试
通过模拟内存压力来观察缓存是否自动失效。注意:垃圾回收的时机由浏览器引擎决定,通常无法强制立即触发,但我们可以通过模拟引用断开来观察逻辑。
-
实例化缓存对象
创建AutoCleanWeakCache的实例myCache。 -
存入大数据
定义一个大型对象或数组,调用myCache.set('bigData', bigObject)将其存入缓存。const myCache = new AutoCleanWeakCache(); let bigData = { id: 1, data: new Array(1000000).fill('memory_heavy_data') }; myCache.set('user_1', bigData); console.log('First get:', myCache.get('user_1')); // 应该能获取到数据 -
断开外部强引用
将变量bigData设置为null,切断外界对该对象的强引用。此时,该对象仅被WeakRef持有。bigData = null; -
观察回收效果
由于 JavaScript 的垃圾回收是惰性的,对象可能不会立即消失。执行myCache.get('user_1')。- 如果 GC 还没发生,你依然能取到值。
- 当内存紧张或 GC 运行后,
deref()返回undefined,且控制台会输出注册表中定义的“Key 已自动清理”日志,Map 也会随之变空。
第五阶段:使用弱引用的注意事项
虽然 WeakRef 很强大,但使用不当会导致难以排查的 Bug。请严格遵守以下原则。
-
不要将关键数据存放在 WeakRef 中
由于对象随时可能被回收,禁止将“必须存在”的业务数据(如用户登录状态、关键配置项)仅用 WeakRef 保存。它只能作为“锦上添花”的缓存层。 -
避免依赖 deref 的返回值做逻辑判断
不要写出if (ref.deref() === undefined) { // 意味着被回收了 }这样的代码。因为undefined也可能是你存入的合法值。正确的做法是仅将其作为缓存读取。 -
不要预判垃圾回收时机
禁止尝试手动触发垃圾回收(虽然在 Node.js 某些模式下可行,但在浏览器中是不可能的)。代码逻辑必须假设对象可能随时存在,也可能随时消失。 -
清理注册表
如果你的缓存键是动态生成的且数量巨大,记得在手动删除缓存项时,也要调用registry.unregister(key),防止注册表中堆积过多的监听器。delete(key) { this.cache.delete(key); // 手动注销监听,防止内存泄漏 this.registry.unregister(key); }
通过以上步骤,你已经构建了一个具备内存自动管理能力的缓存系统,既利用了缓存提升性能,又利用弱引用机制保证了系统的长期稳定性。

暂无评论,快来抢沙发吧!