【JavaScript】面试必知——发布订阅模式

37 阅读4分钟

发布订阅是什么?

发布订阅模式(Publish/Subscribe Pattern)是一种设计模式,它定义了一种一对多的关系。

它允许多个观察者同时监听同一个主题对象,在主题对象状态发生变化时,所有观察者都会收到通知,方便观察者执行自己的操作

它通常用于解耦组件之间的依赖关系 -- 发布者不直接接触订阅者,通过中间件来进行通信

买房时的"微信群"故事

想象你正在找一套心仪的房子,但开发商还没有建好。你有两种选择:

  1. 传统方式(观察者模式):每天亲自去售楼处问"房子建好了吗?"
  2. 订阅方式(发布订阅模式):在售楼处被拉入微信通知群,开发商建好房子后会在群里发消息通知你

这两种方式的本质区别在于:

  • 传统方式需要你主动联系售楼处(强耦合)
  • 订阅方式通过售楼处微信群进行沟通(松耦合)

接下来模拟事件的代码我们不做详细分析,大家知道原理后可以自己试着敲一敲


📖 购房者A

购房者A每天蹲在售楼处问:"房子建好了吗?"
销售终于看不下去了:"来,我拉你进群!"

class EventBus {
  constructor() {
    this.events = {};
  }

  // 拉人入群
  on(eventName, callback) {
    if (!this.events[eventName]) this.events[eventName] = [];
    this.events[eventName].push(callback);
    console.log(`【${eventName}】群新增成员`);
    return this;
  }
}

// 购房者A被拉入群聊
const eventBus = new EventBus();
eventBus.on('houseReady', address => {
  console.log(`购房者A收到通知:${address}的房子已建好!`);
});

📖 购房者B

购房者B在各种原因下需要退出群聊:

class EventBus {
  constructor() {
    this.events = {};
  }

  //...
  // 退群
  off(eventName, callback) {
    if (!this.events[eventName]) return this;
    this.events[eventName] = this.events[eventName].filter(cb => cb !== callback);
    console.log(`🚪【${eventName}】群成员已退出`);
    return this;
  }
}

// 购房者B退群
setTimeout(() => {
  eventBus.off('houseReady', address => {
    console.log(`🚪 购房者B退出群聊`);
  });
}, 2000);

📖 购房者C

购房者C看到售楼处热闹非凡,也想加入

但发现房子早就建好了,只能叹气:"我错过了..."

class EventBus {
  constructor() {
    this.events = {};
  }

  // 入群
  on(eventName, callback) {
    if (!this.events[eventName]) this.events[eventName] = [];
    this.events[eventName].push(callback);
    console.log(`✅【${eventName}】群新增成员`);
    return this;
  }

  // 发布通知
  emit(eventName, data) {
    if (!this.events[eventName]) return this;
    this.events[eventName].forEach(cb => cb(data));
    console.log(`📢【${eventName}】群发送新消息`);
    return this;
  }
}

// 延迟加入会错过通知
setTimeout(() => {
  eventBus.on('houseReady', address => {
    console.log(`🏃‍♀️ 购房者C收到通知:${address}的房子已建好!`);
  });
}, 3000);

// 开发商发布通知(此时只有A和B能收到)
setTimeout(() => {
  eventBus.emit('houseReady', '朝阳区幸福路88号');
}, 1000);

📖 购房者D

购房者D说:"我只要知道这一楼盘房子什么时候建好就行!"

D在得到消息之后就默默退群:

class EventBus {
  constructor() {
    this.events = {};
  }

  // 入群
  on(eventName, callback) {
    if (!this.events[eventName]) this.events[eventName] = [];
    this.events[eventName].push(callback);
    console.log(`✅【${eventName}】群新增成员`);
    return this;
  }

  // 一次性订阅(只收一次通知)即出发后自动退群
  once(eventName, callback) {
    const onceCallback = (...args) => {
      callback(...args);
      this.off(eventName, onceCallback);
      console.log(`🔚【${eventName}】群已自动退群`);
    };
    this.on(eventName, onceCallback);
    return this;
  }
  
}

// 购房者B临时入群
eventBus.once('houseReady', address => {
  console.log(`🎯 购房者B收到首次通知:${address}的房子已建好!`);
});

在代码世界里,实现自己加群又主动退群的方式有些绕。

根据代码,在订阅事件时使用了一个外层包裹函数作为发布通知后被触发的回调函数。

在发布者发布消息后,触发订阅者事件,随即调用off将自己的外层函数删除,做到单次订阅


完整故事线

故事章节对应方法功能说明
购房者Aon长期蹲守群聊
购房者Bonce临时入群
购房者Con延迟加入
购房者Doff主动退群
开发商emit群发通知

小拓展:发布订阅也可以作为异步操作的解决方案

根据发布订阅的机制可知,在消息未发布时,永远不可能会触发订阅者其中的事件。这就做到了解决异步操作最核心的顺序问题。有点类似使用回调函数进行异步操作

  1. 将需要稍后执行的操作订阅某一个事件
  2. 在前置事件为未完成时不发布事件,此时后置事件会被挂起,永远不会触发
  3. 在前置事件完成后发布对应事件,此时后置事件才会被触发

观察者模式

其实在顾客A一直和销售沟通时的状态其实是另一种与发布订阅极其相似的模式,叫做观察者模式。它通常是观察者和被观察者直接进行通信,不需要借助群聊(中间件)。当被观察者发布通知,观察者会自动更新状态。


观察者模式 vs 发布订阅模式 -- 戳我看详情

维度发布订阅模式(群聊)观察者模式(直接联系)
结构有独立的事件中心(微信群)直接联系(电话沟通)
通信方式发布者→事件中心→订阅者被观察者→观察者
典型场景Vue组件通信、消息队列、微前端DOM事件监听、状态管理

更多关于观察者模式可以看看链接文章。这里由于篇幅限制,暂时不仔细展开了


四、总结

通过这个买房故事,我们看到:

  • 发布订阅模式就像一个智能的微信群,让购房者和开发商完全解耦
  • on方法是拉人入群,off方法是退群,once方法是"只收一次通知"
  • 这种设计模式在现代前端开发中尤为重要,帮助我们构建可维护、可扩展的应用程序