观察者模式与发布订阅

这是前端使用频率最高的模式:DOM 事件、Vue 响应式、Redux 订阅、EventBus、Node 的 EventEmitter、RxJS,本质上都是它。

一、观察者 vs 发布订阅

两者常被混为一谈,核心差别在于有没有中间人:

观察者模式:   Subject ─────直接持有────→ Observer
发布订阅模式: Publisher ──→ Event Channel ──→ Subscriber
                        (两端互不知道对方)
观察者模式发布订阅模式
双方是否知道对方Subject 持有 Observer 列表,知道通知谁发布者和订阅者互相不知道,只认识事件中心
耦合度松耦合,但仍有直接引用完全解耦
调度方式通常同步:触发即调用常可异步(消息队列、微任务)
典型实现Vue 的 Dep / Watcher、MutationObserverEventEmitter、EventBus、Redis Pub/Sub

严格来说发布订阅是观察者模式的变体,多引入了一层事件中心。日常交流中不必过分纠结,但面试问到时要能说清"有没有中间调度层"这个差别。

二、观察者模式实现

class Subject {
    constructor() {
        this.observers = new Set();     // 用 Set 天然去重,避免重复订阅
    }

    addObserver(observer) {
        this.observers.add(observer);
        return () => this.removeObserver(observer);   // 返回取消函数,比记名字更可靠
    }

    removeObserver(observer) {
        this.observers.delete(observer);
    }

    notify(message) {
        // 遍历副本,防止回调里增删观察者导致遍历行为异常
        [...this.observers].forEach(observer => {
            try {
                observer.update(message);
            } catch (err) {
                console.error('observer 执行出错:', err);   // 一个观察者出错不应中断其他观察者
            }
        });
    }
}

class Observer {
    constructor(name) { this.name = name; }
    update(message) { console.log(`${this.name} 收到:`, message); }
}

const subject = new Subject();
const a = new Observer('A');
const unsubscribe = subject.addObserver(a);
subject.addObserver(new Observer('B'));

subject.notify('hello');   // A 收到: hello / B 收到: hello
unsubscribe();
subject.notify('again');   // 只有 B 收到

用数组 + findIndex 移除时的经典 bug

removeObserver(observer) {
    const index = this.observerList.findIndex(o => o.name === observer.name);
    this.observerList.splice(index, 1);      // ❌
}

findIndex 找不到时返回 -1,而 splice(-1, 1) 会删掉数组的最后一个元素——移除一个不存在的观察者,反而误删了别人。必须先判断:

if (index !== -1) this.observerList.splice(index, 1);

另外用 o.name === observer.name 做比较,会让同名的不同观察者互相误删,应该比较对象引用本身。

三、发布订阅(EventEmitter)实现

class EventEmitter {
    constructor() {
        this.events = new Map();     // eventName -> Set<handler>
    }

    on(event, handler) {
        if (!this.events.has(event)) this.events.set(event, new Set());
        this.events.get(event).add(handler);
        return this;
    }

    once(event, handler) {
        const wrapper = (...args) => {
            this.off(event, wrapper);    // 先解绑,避免 handler 内部再次 emit 造成重入
            handler.apply(this, args);
        };
        wrapper.raw = handler;           // 记住原函数,才能被 off(event, handler) 正确移除
        return this.on(event, wrapper);
    }

    off(event, handler) {
        const handlers = this.events.get(event);
        if (!handlers) return this;
        if (!handler) {                  // 不传 handler 时移除该事件的全部监听
            this.events.delete(event);
            return this;
        }
        for (const h of handlers) {
            if (h === handler || h.raw === handler) handlers.delete(h);
        }
        if (handlers.size === 0) this.events.delete(event);
        return this;
    }

    emit(event, ...args) {
        const handlers = this.events.get(event);
        if (!handlers || handlers.size === 0) return false;
        [...handlers].forEach(h => h.apply(this, args));   // 遍历副本,兼容 once 的自我移除
        return true;
    }
}

用法:

const bus = new EventEmitter();
bus.on('login', user => console.log('欢迎', user.name));
bus.once('init', () => console.log('只执行一次'));
bus.emit('login', { name: 'Tom' });
bus.emit('init');
bus.emit('init');   // 不再触发

三个必须处理的工程问题

  1. 内存泄漏:on 了就必须在组件卸载时 off。EventBus 是前端内存泄漏的头号来源——被订阅的闭包持有组件引用,导致整个组件树无法回收。
  2. 错误隔离:一个 handler 抛错会中断后续 handler,生产实现要 try/catch。
  3. 事件名硬编码:字符串事件名无法被 IDE 追踪、拼错了也不报错。用常量或 TS 的事件映射类型约束:
    type Events = { login: [user: User]; logout: [] };
    class TypedEmitter<E extends Record<string, unknown[]>> {
        on<K extends keyof E>(event: K, handler: (...args: E[K]) => void) { /* ... */ }
    }
    

Vue 3 移除了 $on / $off / $emit(实例事件)

理由正是上面这些问题:EventBus 让数据流向变得不可追踪——出了问题不知道是谁发的、谁在听。现在推荐用 props/emit、provide/inject 或状态库(Pinia)来传递数据,只在真正需要跨越层级的全局通知时才用事件总线(如 mitt)。

四、在框架中的样子

Vue 2 响应式是标准的观察者模式:

数据 getter  →  Dep.depend()      收集当前的 Watcher(依赖收集)
数据 setter  →  Dep.notify()      通知所有 Watcher 更新(派发更新)

Dep 是 Subject,Watcher 是 Observer。Vue 3 换成 Proxy + effect,思路完全一致,只是收集依赖的方式从 defineProperty 变成了 Proxy 的 trap(见代理模式)。

Redux 的 store.subscribe、MobX 的 autorun、RxJS 的 Observable.subscribe 也都是同一个模式。RxJS 在此之上加了操作符(map / filter / debounceTime),把事件当成"随时间变化的流"来处理。

五、与中介者模式的关系

发布订阅的"事件中心"和中介者很像,区别在于:

  • 事件中心是"哑"的:它只负责转发,不知道业务含义;
  • 中介者是"聪明"的:它封装了对象之间的协作逻辑(A 变了以后 B 该怎么办,由中介者决定)。

下一步 👉 结构型模式

上次更新:
贡献者: Joe