设计模式 - js 发布订阅模式

1,230 阅读6分钟

「这是我参与2022首次更文挑战的第15天,活动详情查看:2022首次更文挑战

在学习中有这样一段话:在学一样东西的时候,可以按照它是什么,有什么用,优点是什么,缺点是什么,要怎么去使用,或者使用的时候要注意什么。我觉得这五部曲还是很有用的,能帮你快速掌握一个知识点

发布订阅模式是什么?

发布-订阅模式其实是一种对象间一对多的依赖关系,当一个对象的状态发送改变时,所有依赖于它的对象都将得到状态改变的通知。

订阅者 把自己想订阅的 事件 注册到 调度中心,当 发布者 发布该 事件调度中心,也就是该事件触发时,由 调度中心 统一调度 订阅者 注册到 调度中心 的处理代码。

比如说我们上淘宝买一样东西,但是价格有点贵,所以我们可以把它加入淘宝的降价通知功能中去,这样子,当商家降低该商品的价格的时候,我们就能够收到对应的淘宝发出的通知。

在这其中,淘宝就是一个 调度中心,我们客户就是 订阅者,商家就是 发布者,我们想要买某一件商品,就相当于是对这件商品添加一个事件:订阅这个商品降价了这个事件,然后这个事件就会被保存到淘宝中去,当商家降低了这件商品的价格,就相当于是 发布了商品价格降低这个事件,然后我们保存在淘宝中的事件就会被淘宝调用,也就是淘宝会通知我们商品降价了。

发布订阅模式有什么用?

解决了一个对象方式改变其他对象通知的问题,解耦他们之间的依赖关系

从上举的例子来看,我们就很好理解发布订阅模式的作用。你要买某样东西,你不需要在一直去关注物品的价格是否降低,会由淘宝来通知你,你只需要等待淘宝的信息,这样就实现了你和商家之间的解耦,你都不需要去知道商家的商品是涨价了还是降价了,反正当它一降价,淘宝就去通知你。

在我们的项目当中,就是你可以定义一个 调度中心 ,然后在需要接受信息的地方他就是 订阅者,发布信息的地方就是 发布者,然后你只需要定义发布什么事件,然后定义什么事件,并且在这个事件发布的时候,你要去执行什么操作。

image.png

像上图,发布者通过调度中心提供的 emit 方法去发布一个事件,订阅者通过 on 方法去订阅一个事件。

那么发布者不需要知道谁会对这个事件作出响应,订阅者也不需要知道这个事件什么时候要触发,它们之间的通信都由调度中心来帮它们完成。

发布订阅模式的优点?

  1. 解耦。像上方提到过的,通过发布订阅模式进行通信,通信双方不需要有任何的联系,只需要关注彼此的事件即可。

  2. 广泛应用于异步编程当中。在异步编程中,我们是不知道结果什么时候会返回的,通常的办法就是通过传入一个回调函数,在异步结束后,调用这个回调函数,有值的话就将值作为回调的参数传入。但是在发布订阅中,只需要在外部去订阅一个事件,比方说 'ready' 事件,那么在异步结束的时候,就可以发布 'ready' 这个事件,那么订阅放就会执行它订阅的函数,并且发布事件也是可以传递参数的,这个后面具体实现再来说说。

发布订阅模式的缺点?

  • 就如同优点所说,将两个对象解耦不仅是他的优点,也是它的缺点所在:
  1. 因为发布者不会去关心事件发布的结果,所以一旦事件发布结束想要进行反馈就没有办法。比方说上方的 'ready' 事件发布之后,订阅者做出了对应的操作,但是你没有办法通知发布者你执行了什么操作,你只能通过发布事件的方式在发一个事件回去。

那么发布订阅模式在实际的代码当中要怎么去使用呢?

首先我们需要定义一个调度中心 EventBus,这个调度中心中 有一个用于存放订阅事件的回调的 list ,还有一个用于发布事件的 emit 方法,还有就是用于订阅某个事件的 on 方法,既然能够订阅事件,那么就应该也要能够取消事件的订阅,所以就还需要一个取消订阅的 off 方法:

export class EventBus {
  list = {};
  on(key, fn) {
    if (!this.list[key]) {
      this.list[key] = [];
    }
    this.list[key].push(fn);
  }
  off(key, fn) {
    let fns = this.list[key];
    if (!fns) return false;
    if (!fn) {
      fns && (fns.length = 0);
    } else {
      fns.forEach((cb, i) => {
        if (cb === fn) {
          fns.splice(i, 1);
        }
      });
    }
  }
  emit() {
    let key = [].shift.call(arguments),
      fns = this.list[key];
    if (!fns || fns.length === 0) {
      return false;
    }
    fns.forEach((fn) => {
      fn.apply(this, arguments);
    });
  }
}

这样我们就可以将它用于实际应用中去:

import { eventBus } from "./fabudingyue.js";

class Publisher {
  constructor() {
    this.lowPrice();
  }

  lowPrice() {
    eventBus.emit("ready", 100);
  }
}

class Subscriber {
  constructor() {}
  getLowPrice() {
    eventBus.on("ready", (data) => {
      console.log("ready", data);
    });
  }
}

const subscriber = new Subscriber();

const publisher = new Publisher();

像上方的 subscriber 在实例化的时候会去监听 "ready" 事件,而 publisher 实例化完成后会发布 "ready" 事件,那么就会调用 subscriber 中的方法,输出 "ready" 以及传过来的 100 这个值。

那么以上就是一个最简单的可使用的发布订阅模式了,实现一个发布订阅主要分为4步:

  • 创建一个 List (缓存列表)
  • on方法用来把回调函数fn都加到缓存列表中
  • emit方法将传入的事件名称作为 key 去执行缓存列表中对应的函数
  • remove方法可以根据key值取消订阅

总结

发布订阅模式在很多项目中都会去使用,因为它是的两个对象之间的关联做了一个解耦,这样就不会造成代码里面对象与对象之间的强耦合,每个不同的对象之间都是通过数据交互的方式进行,这样的代码也更加的利于维护。