【设计模式】发布订阅模式,构建可以接收历史消息的观察者类

2024/04/25 14:59:242022/12/26 15:29:13

什么是发布订阅模式?

发布订阅模式也被称为观察者模式,这个模式中有两种角色:发布者(被观察者)订阅者(观察者)

通常的操作是:订阅者订阅发布者的某一个事件,发布者接收到这个事件变化的时候,通知所有的订阅者。

举个栗子:你在微信中关注了一个公众号,这个公众号在更新文章的时候,微信会将新的文章推送到你的微信消息中,公众号是发布者,你的微信号是订阅者。

你可以关注多个公众号,公众号也可以有多个粉丝。

一个简单的观察者类

class Observer {
  /**
   * 事件队列
   */
  static events = {};

  /**
   * 订阅
   * @param {*} topic
   * @param {*} callback
   */
  static subscribe(topic, callback) {
    if (!Observer.events[topic]) {
      Observer.events[topic] = [];
    }
    Observer.events[topic].push(callback);
    return () => {
      const index = Observer.events[topic].findIndex(
        (item) => item === callback
      );
      Observer.events[topic].splice(index, 1);
    };
  }

  /**
   * 发布
   * @param {*} topic
   * @param  {*} data
   */
  static publish(topic, data) {
    if (Observer.events[topic]) {
      Observer.events[topic].forEach((fn) => {
        fn(data);
      });
    } else {
      console.error(`topic [${topic}] is empty!`);
    }
  }
}

订阅消息:

// 订阅消息
const unsubscribe = Observer.subscribe("update", (data) => {
  console.log(data);
});

// 取消订阅
unsubscribe();

发布消息:

Observer.publish("update", 1);

可以接收历史消息的观察者类

上面的观察者无法接收历史消息,如果一个消息在未订阅时就已经发布,那么这条消息就会被漏掉。

一个可能的场景是:在一个页面中,需要在导航栏中展示用户信息,用户信息需要通过网络请求来获取,获取到后通过 Observer.publish() 方法发布,导航栏通过 Observer.subscribe() 方法来获取用户信息及回调。

这个场景中如果用户信息已经获取,但是导航栏组件还未加载,这种情况下导航栏就再也拿不到用户信息了。

解决方案就是维护一个历史消息列表,在调用 Observer.subscribe() 订阅消息时如果有历史消息则立即触发回调。

class Observer {
  /**
   * 历史消息
   */
  static history = {};

  /**
   * 事件队列
   */
  static events = {};

  /**
   * 订阅
   * @param {*} topic
   * @param {*} callback
   */
  static subscribe(topic, callback) {
    if (!Observer.events[topic]) {
      Observer.events[topic] = [];
    }
    Observer.events[topic].push(callback);

    // 如果有历史消息
    if (Observer.history[topic]) {
      callback(Observer.history[topic]);
    }

    return () => {
      const index = Observer.events[topic].findIndex(
        (item) => item === callback
      );
      Observer.events[topic].splice(index, 1);
    };
  }

  /**
   * 发布
   * @param {*} topic
   * @param  {...any} args
   */
  static publish(topic, data) {
    if (Observer.events[topic]) {
      Observer.events[topic].forEach((fn) => {
        fn(data);
      });
    }

    if (!Observer.history[topic]) {
      Observer.history[topic] = [];
    }

    // 保存历史消息
    Observer.history[topic].push(data);
  }
}

可以先发布后订阅:

Observer.publish("update", 222);
Observer.publish("update", 333);

const unsubscribe = Observer.subscribe("update", (data) => {
  if (Array.isArray(data)) {
    // 历史消息列表
  } else {
    // 最新消息
  }
  console.log(data);
});

Observer.publish("update", 444);

总结

发布订阅模式的优点是可以很方便的实现不同模块之间的通信。

它的缺点在于,观察者对象本身是占用内存的,而且当你订阅一个消息后,也许此消息再也没有发布过,但这个观察者对象会始终存在于内存中。

发布订阅模式弱化了对象之间的联系,但如果过度使用的话,对象和对象之间的必要联系也将被深埋在背后,会导致程序难以跟踪维护和理解。

当多个发布者和订阅者嵌套到一起的时候,很难捋清楚他们之间的关系。

可以用,但别到处都用。

参考

《JavaScript 设计模式与开发实践》