先记住这个答案
观察者模式常用数组保存监听器,若忘记退订会强引用目标,导致内存泄漏。WeakMap 以主题对象为键、订阅集合为值,不阻止主题回收;主题消失后条目自动可回收,监听器若无引用也被回收,从而免除退订操作。WeakRef 可进一步持有弱引用目标,但需配合 FinalizationRegistry 清理。
- WeakMap 键为弱引用,不阻止对象回收。
- 主题对象消亡后,其订阅条目可被回收。
- WeakMap 不可枚举,不能遍历订阅列表。
弱引用如何让订阅自动失效
常规 Map 保存订阅时,键(主题对象)被 Map 强引用。即使主题对象在业务中已不可达,Map 仍使其存活,导致依赖它的监听器集合也一直存活,形成泄漏。WeakMap 则不同:它对键只保持弱引用,不参与垃圾回收可达性判定。主题对象只要没有其它强引用,即便仍在 WeakMap 中,也会被回收。
回收后,该键对应的值(如监听器数组)不再有引用链(键已死),除非值被外部持有,否则也会在下一轮回收。这就让订阅表随主题对象生命周期自动清理。但 WeakMap 不提供键的枚举或大小,因为键的存活状态取决于 GC,不可预测,相关 API 被刻意省略。
页面组件与数据源订阅
观察者模式中常见的泄漏是主题(被观察对象)长生命周期、订阅者短生命周期,主题通过订阅回调间接持有订阅者。WeakMap 的弱引用只能针对键,若直接以长生命周期主题为键存储回调集合,主题仍强引用回调,回调若捕获了短生命周期订阅者则泄漏依旧。WeakMap 更适用于主题自身生命周期较短的场景,例如临时任务或一次性事件源。下面示例模拟这种用法。
代码中 topic 是临时对象,当外部不再引用后,WeakMap 条目变为不可达,其值(回调集合)在没有其他引用时也会被回收,因此无需显式退订。但若主题是长期存活的共享数据源,且回调闭包引用了短生命周期视图,即使视图销毁,主题通过该 WeakMap 的 value 仍持有回调,从而间接持有视图——此时应使用 WeakRef 让主题仅弱持有订阅者,或在视图销毁时显式退订,不能指望键回收。
const subs = new WeakMap();
function subscribe(topic, callback) {
if (!subs.has(topic)) subs.set(topic, new Set());
subs.get(topic).add(callback);
return () => subs.get(topic)?.delete(callback);
}
function notify(topic, data) {
subs.get(topic)?.forEach(cb => cb(data));
}
let topic = { name: 'temp' };
subscribe(topic, data => console.log('收到', data));
notify(topic, '第一次'); // 收到 第一次
topic = null; // 不再持有主题
// 下次 GC 后,WeakMap 条目被清除
// 但此处无法观测删除,因为 GC 不确定查看输出与解释
收到 第一次该代码模拟短生命周期主题。设置 null 后,weakMap 条目在后续 GC 被回收。不打印删除事件,因不可见。
适用边界与代价
WeakMap 只能以对象作为键,且不能遍历或清空。如果业务需要列出当前所有主题或手动取消所有订阅,WeakMap 无法直接实现。另外,值仍会强引用回调函数,如果回调是长生命周期对象的方法,不解除引用仍会泄漏。WeakMap 只处理主题对象的生命周期,不处理订阅者自身的生命周期。
WeakRef 能直接持有订阅者的弱引用,但使用复杂且需要 FinalizationRegistry 清理参考,否则可能出现回调触发时目标已死的情况。实际工程中,优先使用显式退订,更可控。WeakMap 适合主题对象天然短生命周期或框架层确保主题不再引用时自动回收。
容易答错的地方
- 以订阅者为键即可防泄漏
- 错误。若键是订阅者,值中通常含主题引用,主题仍被值强引用。应反转:以被观察的主题为键,回调中对订阅者弱引用。
- WeakMap 可枚举或清空
- 错误。WeakMap 没有 keys()、values()、size 和 clear(),因为键存活依赖 GC,不确定。需要枚举时改用 Map 加手动操作。
面试官还会怎么问?
WeakRef 相比 WeakMap 有什么不同应用?
WeakRef 允许你持有任意对象的弱引用,可以单独用于订阅者列表。但需在取用时检查是否已被回收,并用 FinalizationRegistry 清理相关注册表数据,代码复杂度高。
主题对象被回收,但监听器仍被用户引用,会发生什么?
WeakMap 条目删除后,监听器集合不再被引用,但监听器本身如果你还保存着变量,仍存活。因为它们被外部引用。泄漏只发生在监听器也无需存活时。
可以用 WeakMap 实现订阅总列表吗?
不能。WeakMap 不可枚举,所以无法统计或遍历所有订阅。若需要列表,应维护一个 Map 并显式清理。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。