深入浅出:JavaScript 设计模式之观察者模式与发布订阅模式的区别

254 阅读6分钟

1.前言

观察者模式与订阅发布模式只是 JavaScript 的两种设计模式。那在我们日常开发中什么场景下发现我需要用设计模式呢?我看到一个非常形象的比喻,不要为了用设计模式而用,而是发现了病症(代码问题),我需要用药方(设计模式)来解决了。那当我发现一个对象需要通知多个其他的对象,但是又不想将他们紧紧地绑定在一起的时候,就可以考虑使用观察者或者订阅发布模式。

2.是什么

2.1 观察者模式

观察者模式是定义了一种直接依赖通信的设计模式,包含目标者(Subject)和观察者们(Observers)。当目标者发生变化时,对它有依赖关系的观察者们会收到消息并自动更新。

2.2 订阅发布模式

订阅发布模式是定义了一种间接通信的设计模式,包含订阅者(Subscriber)、中间层(事件总线)(Event Bus)和发布者(Publisher)。订阅者在中间层订阅事件,发布者发布事件到中间层,中间层负责接收发布者的事件,根据事件的类型,将其过滤分发给对此类事件感兴趣的订阅者。

3.使用场景

3.1 观察者模式

  • 通信双方已知并固定,关系紧密,且只有一个逻辑单元
  • 逻辑上属于同一个模块或者系统
  • 通知是同步、立即更新的

3.2 发布订阅模式

  • 通信双方不固定,关系灵活,逻辑复杂
  • 通信需要跨模块/组件/系统的边界

4.优缺点

4.1 观察者模式

  • 优点:

    ✅ 极其高效,同步立即执行
    ✅ 强一致性,目标者改变,能确定所有的观察者都会更新状态
    ✅ 简洁可读性高,没有中间层,逻辑简单

  • 缺点:

    ❌ 耦合较高,目标者需要清楚地知道所有的观察者,观察者也需要知道目标者的存在并注册,这种双向知晓构成了耦合
    ❌ 不异于扩展,如果需要增加新的观察者,需要在更改目标者的代码来支持。
    ❌ 同步执行可能会有阻塞问题,如果其中一个观察者的更新比较耗时,那么会堵塞其他观察者的状态更新

4.2 发布订阅模式

  • 优点:

    ✅ 极致地解耦,订阅者和发布者无需知道对方的存在
    ✅ 极强的扩展性和灵活性,随时为某个事件添加订阅者,添加新的订阅者无需更改发布者的代码,符合开放-封闭原则
    ✅ 支持异步,中间层容易对事件进行异步处理,避免因耗时的订阅者堵塞整个系统

  • 缺点:

    ❌ 复杂度高,调试难度增加,流程变得复杂
    ❌ 依赖中间层,引入单点故障:一旦中间层发生故障,则整个系统瘫痪
    ❌ 提高性能成本,相较直接的方法,对事件进行封装、传递、解析等有一定的性能开销

5.代码实现

5.1 观察者模式代码实现

class Subject {
    constructor() {
        this.state = null
        this.observers = []
    }
    // 添加观察者
    add(observer) {
        this.observers.push(observer)
    }
    // 移除观察者
    remove(removeObserver) {
        this.observers = this.observers.filter(item => removeObserver !== item)
    }
    // 通知观察者
    notify() {
        this.observers.forEach(item => {
            item.update(this.state)
        })
    }
    // 更新状态并通知观察者
    setState(state) {
        this.state = state
        this.notify()
    }
}

class Observer {

    constructor(name) {
        this.name = name
    }

    update(...args) {
        console.log(`${this.name}观察者状态更新成: ${args}`)
    }
    
}

5.2 发布订阅模式实现

//事件总线
class EventBus {

    constructor() {
        this.events = {}
    }

    // 添加事件
    _add(eventName,callback,isOnce) {
        if(!this.events[eventName]) {
            this.events[eventName] = []
        } 
        this.events[eventName].push({callback,isOnce}) 
    }

    // 事件注册
    on (eventName,callback) {
        this._add(eventName,callback,false)
    }
    // 一次性事件监听
    once (eventName,callback) {
        this._add(eventName,callback,true)
    }

    // 事件触发
    emit(eventName,...data) {
        

        if(!this.events[eventName]) return 

        const eventList = this.events[eventName]

        // 创建副本,防止在删除元素过程中产生异常
        const listeners = [...eventList]

        listeners.forEach(({callback,isOnce},index) => {
            
            callback(...data)  
            
            if(isOnce){
                this.off(eventName,callback)
            }
        })
    }

    // 事件移除
    off(eventName,callback) {

        const eventList = this.events[eventName]

        if(!eventList) return 

        this.events[eventName] = eventList.filter((item,index)=> item.callback!==callback)
    }
}

module.exports = EventBus


6.测试验证

单元测试,使用了Jest框架,下面是测试的几个测试用例,完全通过

6.1 观察者模式测试验证

const sub = new Subject()

const obsA = new Observer("观察者A")
const obsB = new Observer("观察者B")
const obsC = new Observer("观察者C")

// 注册
sub.add(obsA)
sub.add(obsB)
sub.add(obsC)

// 移除 B
sub.remove(obsB)
// 更新状态
sub.setState("我饿了")
// 观察者A观察者状态更新成: 我饿了
// 观察者C观察者状态更新成: 我饿了

6.2 发布订阅模式测试验证

const EventBus = require('../xxxx');

describe('EventBus 发布订阅模式测试', () => {
  let eventBus;
  
  // 每个测试前创建新的EventBus实例,避免测试间相互影响
  beforeEach(() => {
    eventBus = new EventBus();
  });

  test('应该能订阅并发布事件', () => {
    const callback = jest.fn();
    
    // 订阅事件
    eventBus.on('test-event', callback);
    
    // 发布事件
    eventBus.emit('test-event', 'hello', 'world');
    
    // 验证回调被调用且参数正确
    expect(callback).toHaveBeenCalledTimes(1);
    expect(callback).toHaveBeenCalledWith('hello', 'world');
  });

  test('一次性订阅应该只触发一次', () => {
    const onceCallback = jest.fn();
    
    // 一次性订阅
    eventBus.once('once-event', onceCallback);
    
    // 第一次发布
    eventBus.emit('once-event', 'first');
    // 第二次发布
    eventBus.emit('once-event', 'second');
    
    // 验证只被调用一次,且收到第一次的参数
    expect(onceCallback).toHaveBeenCalledTimes(1);
    expect(onceCallback).toHaveBeenCalledWith('first');
  });
  test('多次一次性订阅同一个事件,应该每个都触发一次', () => {
    const onceCallback1 = jest.fn();
    const onceCallback2 = jest.fn();
    const onceCallback3 = jest.fn();
    
    // 一次性订阅
    eventBus.once('once-event', onceCallback1);
    eventBus.once('once-event', onceCallback2);
    eventBus.once('once-event', onceCallback3);
    
    // 第一次发布
    eventBus.emit('once-event', 'first');

    // 第二次发布
    eventBus.emit('once-event', 'second');
    
    // 验证只被调用一次,且收到第一次的参数
    expect(onceCallback1).toHaveBeenCalledTimes(1);
    expect(onceCallback2).toHaveBeenCalledTimes(1);
    expect(onceCallback3).toHaveBeenCalledTimes(1);
    expect(onceCallback1).toHaveBeenCalledWith('first');
    expect(onceCallback2).toHaveBeenCalledWith('first');
    expect(onceCallback3).toHaveBeenCalledWith('first');
  });

  test('应该能取消订阅', () => {
    const callback = jest.fn();
    
    // 订阅事件
    eventBus.on('cancel-event', callback);
    // 取消订阅
    eventBus.off('cancel-event', callback);
    
    // 发布事件
    eventBus.emit('cancel-event', 'should not trigger');
    
    // 验证回调未被调用
    expect(callback).not.toHaveBeenCalled();
  });

  test('发布未被订阅的事件应该无副作用', () => {
    // 测试发布未订阅的事件不会报错
    expect(() => {
      eventBus.emit('unsubscribed-event', 'test');
    }).not.toThrow();
  });

  test('多个订阅者应该都能收到事件', () => {
    const callback1 = jest.fn();
    const callback2 = jest.fn();
    
    eventBus.on('multi-subscribers', callback1);
    eventBus.on('multi-subscribers', callback2);
    
    eventBus.emit('multi-subscribers', 'shared data');
    
    expect(callback1).toHaveBeenCalledTimes(1);
    expect(callback2).toHaveBeenCalledTimes(1);
    expect(callback1).toHaveBeenCalledWith('shared data');
    expect(callback2).toHaveBeenCalledWith('shared data');
  });
});
    
    

7.具体的技术场景

7.1 观察者模式技术场景

Vue 2 的响应式系统

  • Subject:一个 Vue 组件的 data 对象(背后的 Dep 类)。
  • Observers:依赖于这个数据的计算属性(Computed) 和模板渲染函数(Watcher)。
    为什么用它:关系非常直接和固定。一个数据变了,就知道要通知哪些计算属性和渲染函数。这是 Vue 内部机制的核心,需要极高的性能和同步性。

7.2 发布订阅模式技术场景

Vue 的全局事件总线(emit/emit / on)

  • 场景:两个毫无关联的 Vue 组件(比如一个在页面头部 Header,一个在侧边栏 Sidebar)需要通信。
  • 过程:Header 组件发布一个 'toggle-theme' 事件。Sidebar 组件订阅了 'toggle-theme' 事件。它们不需要互相引用,只通过全局的 $bus 通信。
    为什么用它:完全解耦。Header 和 Sidebar 是独立的,不应该有直接引用。

8.总结

没有最好的模式,只有最合适的模式。追求性能和控制力,处理内部状态流,选择观察者模式。追求架构灵活性和可扩展性,需要跨边界通信,选择发布订阅模式。一个大型系统可能会同时包含两种模式,观察者模式处理内部核心的、高效状态的变化,发布订阅模式处理外部的,松散的事件通信。