You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 07:31:42