观察者模式(发布订阅模式)

什么是观察者模式 ?

在现实生活中,我们使用微信去订阅一些微信公众号。而微信公众号也可以被多个用户订阅。而这些用户就是微信公宗号的订阅者。当微信公众号发布新文章会通知所有订阅者。

从上面例子来看,观察者模式中存在两种角色 发布者 和 订阅者。当发布者的信息更新时,会通知订阅者。

简单实现观察者模式 ?

从上面例子来看,我们应该有个对象,这个对象应该对为提供如下功能

  • 添加订阅
  • 取消订阅
  • 通知订阅者(私有)

我们先写发布者对象。

const publisher = {
  observers: [],
  message: "publisher",
  // 添加订阅者
  add(observer) {
    this.observers.push(observer);
  },
  // 移除订阅者
  remove(observer) {
    const idx = this.observers.findIndex(observer);
    if (idx !== -1)
      this.observers.splice(idx, 1);
  },
  // 通知所有订阅者
  notity(...args) {
    this.observers.forEach(observer => observer(...args));
  },
  // 修改message属性
  setMessage(msg) {
    this.message = msg;
    // 数据发生改变通知订阅者
    this.notity(msg);
  }
};

在这段代码中,当我们修改message属性,他会通知其所有订阅者执行,并将message信息推给订阅对象。

接下来是订阅者

// 订阅者, 在js中因为函数是一等公民,所以我们能够很轻易将函数当作订阅者执行。
function observer1(...args) {
  console.log(observer1.name, args.toString());
}

function observer2(...args) {
  console.log(observer2.name, args.toString());
}

因为函数在js中是一等公民。我们可以把函数当成订阅者。当发布者发布订阅者需要的信息,就执行该订阅函数。

上面的代码实现的比较简单。存在一些缺陷

  • 并没有为订阅者提供订阅事件进行分类,当发布者任何信息发生改变的时候都将通知所有订阅者。
  • 其次我们如果需要订阅很多发布者,我们得一个个定义。发布者和订阅者是强耦合关系。

接下来我们来解决这两个问题。

为订阅者提供订阅事件进行订阅者分类。

我们大概的数据结构如下

eventName => [observer1, observer2]

修改发布者代码。

const publisher = {
  observers: new Map(),
  message: "publisher",
  // 添加订阅者
  add(eventName, observer) {
    let list = this.observers.get(eventName);
    if (!list) {
      this.observers.set(eventName, (list = []));
    }
    list.push(observer);
  },
  // 移除订阅者
  remove(eventName, observer) {
    const list = this.observers.get(eventName);
    
    if (!list || list.length === 0)
      return;
    if (!observer)
      return;
    
    const idx = list.findIndex(observer);
    if (idx !== -1)
      list.splice(idx, 1);
  },
  // 通知所有订阅者
  notity(eventName, ...args) {
    const list = this.observers.get(eventName) || [];
    list.forEach(observer => observer(...args));
  },
  // 修改message属性
  setMessage(msg) {
    this.message = msg;
    // 数据发生改变通知订阅者
    this.notity("messageChangeEvent", msg);
  }
};

上面代码中,我们使用 Map 数据结构来对订阅者通过订阅事件分类,实现了精准通知。

我们来解决第二个问题,发布者与订阅者强耦合关系。接下来一节将会讨论 观察者模式 和 发布订阅模式 的区别。

观察者模式 和 发布订阅者模式 区别

在本质上 观察者模式 和 发布订阅者模式 的设计理念是一致的。但是,还是存在细微的差别。发布订阅者模式中,在发布者和订阅者,引入了一层订阅中心。发布者只需要将消息发送至订阅中心。然后,订阅中心在通知订阅者。这样,就将发布者和订阅者解耦合了。

image-20230205233202413

我们想象这样一个场景。我们知道在微信中有很多前端相关的公众号,但是都需要我们一个个单独去订阅。如果存在这样一个订阅中心,我们只需要订阅 有前端标签的文章,当公众号发布文章时,只需要通知订阅中心它新发布了一篇带有前端标签的文章。将会通知订阅前端标签的订阅者。

我们接下来实现一下订阅中心,该订阅中心因该是单例模式,在js中对象字面量对象就是单例模式。

const subscriptCenter = {
  observes: new Map(),
  // 发布者发布事件
  emit(articleTag, articleUrl) {
    this.notify(articleTag, articleUrl);
  },
	// 通知对应订阅者
  notify(articleTag, articlUrl) {
    const list = this.observes.get(articleTag) || [];
    list.forEach(observe => observe(articlUrl));
  },
	// 发起订阅
  on(articleTag, observe) {
    let list = this.observes.get(articleTag) || [];
    if (!list) {
      this.observes.set(articleTag, (list = []));
    }
    list.push(observe);
  },
	// 移除订阅
  off(articleTag, observe) {
    const list = this.observes.get(articleTag);
    if (!observe || list.length === 0) 
      return;
    const idx = list.findIndex(observe);
    list.splice(idx, 1);
  }
};

上面代码使得我们的订阅者很容易就能关注它前端标签相关的文章。而不再需要到处订阅其他公众号了。实现了,发布者和订阅者解耦合。

但是,上面的代码完美吗?很显然不完美。发布订阅模式,就不能先发布在订阅吗?还是以上面订阅 前端标签 文章为例。如果用户没有上线,此时用户必然不在订阅列表中,不会收到通知。但是,当用户上线的时候应该能够收到通知。所以,我们必须缓存在没有订阅者时的发布信息,当订阅者订阅时,将其通知到订阅者。

缓存方法有很多,这里就不在介绍了。

观察者模式(发布订阅模式)开源项目中的应用?

Vue 的响应式系统,收集依赖,通知订阅者。我有一篇介绍 响应式系统实现原理

EventBus: https://github.com/greenrobot/EventBus