如何掌握JavaScript中的设计模式_JavaScript观察者模式如何实现事件监听

观察者模式在JavaScript中由EventTarget原生支持,addEventListener/dispatchEvent即其标准实现;手写Observer需注意去重、异步执行和取消订阅;Vue/React的响应式是其思想的深度封装而非直接应用。

观察者模式在 JavaScript 中不是靠「实现」出来的,而是靠语言特性自然支撑的——它本质是事件监听的底层逻辑,addEventListener 就是它的标准封装。

为什么不用手写 Observer 类也能算用了观察者模式

因为浏览器原生的 EventTarget 接口(HTMLElementdocumentwindow 等都继承它)已经完整实现了发布-订阅机制:

  • 调用 addEventListener(type, callback) = 订阅
  • 调用 dispatchEvent(event) = 发布
  • 同一个事件类型可绑定多个回调,触发时全部执行 = 多观察者支持
  • 回调执行顺序遵循注册顺序,且可被 once: trueremoveEventListener 控制生命周期

手写简易 Observer 时最容易漏掉的三个点

如果业务需要脱离 DOM 自建状态通知系统(比如响应式数据、跨模块通信),手写一个轻量 Observer 很常见,但以下细节常被忽略:

  • 没做回调去重:同一函数多次 subscribenotify 时会重复执行 —— 应用 Set 存储回调或手动比对 fn === existing
  • 没处理异步通知顺序:直接在 notify 中同步遍历执行,若某个回调抛错,后续回调中断 —— 建议统一用 Promise.resolve().then(() => fn()) 包裹
  • 没提供取消订阅接口:只留 subscribe 没留 unsubscribe,导致内存泄漏风险 —— 必须返回取消函数,或接受 callback 作为卸载依据

Vue / React 中的「响应式」和观察者模式是什么关系

它们借用了观察者思想,但实现上已远超经典模式:

  • Vue 2 的 Object.defineProperty + 依赖收集,每个响应式属性内部维护一个 Dep 实例(即主题),Watcher 实例(即观察者)在取值时自动订阅 —— 这是编译期+运行期协同的隐式订阅
  • React 的 useStateuseEffect 不暴露订阅 API,更新由 dispatchAction 触发,内部用链表管理更新队列 —— 它更接近「状态驱动重渲染」,观察者逻辑被框架深度封装
  • 二者都不鼓励你手动维护 subscribe/unsubscribe,而是通过 Hook/选项声明式表达依赖 —— 所以别在 React 组件里自己 new Observer,优先用 useReducerContext + useContext
class SimpleObserver {
  constructor() {
    this.callbacks = new Set();
  }
  subscribe(fn) {
    this.callbacks.add(fn);
    return () => this.callbacks.delete(fn);
  }
  notify(data) {
    this.callbacks.forEach(fn => {
      Promise.resolve().then(() => fn(data));
    });
  }
}

// 使用示例
const obs = new SimpleObserver();
const unsubscribe = obs.subscribe(console.log);
obs.notify('hello'); // → 'hello'
unsubscribe();       // 取消监听
obs.notify('world'); // 无输出

真正难的不是写出一个能跑的 Observer,而是判断什么时候不该用它——比如单个组件内的状态变化,用 useState 更直接;两个紧耦合模块通信,用 props / emit 更清晰;只有跨层级、低耦合、多消费者的通知场景,才值得引入显式的观察者抽象。