JavaScript实现观察者模式:弱引用可迭代监听容器的难题
解决JavaScript观察者模式中的弱引用与可迭代需求冲突问题
你碰到的这个矛盾确实是JavaScript里的特有痛点——WeakSet的弱引用能帮我们避免内存泄漏,但它没法迭代遍历;普通Set虽然能循环通知监听者,却会强引用函数,导致像示例里memory这种已经脱离作用域的对象被意外持有,一直占着内存。
我给你两种可行的解决方案,都是原生实现,不用任何外部库:
方案一:用WeakRef + FinalizationRegistry实现自动清理的可迭代弱引用集合
这个方案能自动回收未被使用的监听函数,同时保留遍历通知的能力,完美匹配你的需求。核心思路是用普通Set存储监听函数的弱引用(WeakRef),再用FinalizationRegistry监听这些函数的回收状态,自动清理失效的引用。
const makeObservable = function(obj, value) { // 存储监听函数的WeakRef实例 const listeners = new Set(); // 终结器注册表:当监听函数被GC回收时,自动清理对应的WeakRef const cleanupRegistry = new FinalizationRegistry(weakRef => { if (listeners.has(weakRef)) { listeners.delete(weakRef); } }); obj.addListener = function(listener) { const weakRef = new WeakRef(listener); listeners.add(weakRef); // 注册终结器:监听函数被回收时,通知我们清理WeakRef cleanupRegistry.register(listener, weakRef); }; obj.removeListener = function(listener) { // 遍历找到对应监听函数的WeakRef并删除 for (const weakRef of listeners) { const ref = weakRef.deref(); if (ref === listener) { listeners.delete(weakRef); cleanupRegistry.unregister(listener); break; } } }; obj.setValue = function(newVal) { if (value === newVal) return; const oldVal = value; value = newVal; // 先过滤出所有还存活的监听函数,同时清理失效的WeakRef const validListeners = []; for (const weakRef of listeners) { const listener = weakRef.deref(); if (listener) { validListeners.push(listener); } else { listeners.delete(weakRef); } } // 通知所有存活的监听者 validListeners.forEach(listener => listener(oldVal, newVal)); }; obj.getValue = function() { return value; }; return obj; }; // 测试你的示例代码 const obs1 = makeObservable({ }, 5); const obs2 = makeObservable({ }, 10); (function() { const memory = { }; const lis = function(_, newVal) { memory.changed = newVal; }; obs1.addListener(lis); obs2.addListener(lis); })(); // 当lis和memory失去所有强引用后,GC会自动回收它们,对应的WeakRef也会被清理
关键细节说明:
WeakRef:它创建的是对监听函数的弱引用,不会阻止GC回收那些没有其他强引用的函数FinalizationRegistry:相当于一个"回收监听器",当监听函数被GC销毁时,它会触发我们注册的清理逻辑,把对应的WeakRef从Set中移除- 在
setValue时,我们先过滤出所有还存活的监听函数,确保不会调用已经被回收的函数,同时顺便清理失效的WeakRef,保持集合整洁
方案二:用AbortController主动管理监听生命周期
如果你能接受让监听者主动触发注销(比如在组件销毁时),这个方案会更简洁。它利用AbortController的信号机制,让监听者可以主动取消监听:
const makeObservable = function(obj, value) { const listeners = new Set(); obj.addListener = function(listener, { signal } = {}) { listeners.add(listener); // 如果传入了AbortSignal,监听abort事件来自动移除监听 if (signal) { const handleAbort = () => listeners.delete(listener); signal.addEventListener('abort', handleAbort, { once: true }); } }; obj.removeListener = function(listener) { listeners.delete(listener); }; obj.setValue = function(newVal) { if (value === newVal) return; const oldVal = value; value = newVal; // 直接遍历通知所有监听者 for (const listener of listeners) { listener(oldVal, newVal); } }; obj.getValue = function() { return value; }; return obj; }; // 使用示例 const obs1 = makeObservable({ }, 5); const obs2 = makeObservable({ }, 10); (function() { const controller = new AbortController(); const memory = { }; const lis = function(_, newVal) { memory.changed = newVal; }; // 传入AbortSignal obs1.addListener(lis, { signal: controller.signal }); obs2.addListener(lis, { signal: controller.signal }); // 当作用域结束时,主动触发abort,移除监听 controller.abort(); })();
这个方案的好处是实现简单,逻辑清晰,但需要监听者主动管理注销时机,不像方案一那样完全自动。
总结
如果想要完全自动的内存管理,方案一(WeakRef+FinalizationRegistry)是最佳选择;如果你的场景中可以主动控制监听的生命周期,方案二会更轻便。
内容的提问来源于stack exchange,提问作者Hatefiend
相关产品推荐
相关产品推荐

