[设计模式]手写一个发布订阅模式

50 阅读5分钟

虽然发布订阅模式严格意义来讲不属于设计模式中的一种,但是在各大框架源码中,经常使用发布订阅模式。下面我们来看下怎么实现的吧。

什么是发布订阅模式?

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

举个栗子

当我们在浏览博客论坛之类的网站时,遇到感兴趣的up主,我们会订阅他们的文章,这样一来,他们每次在网站发布一个文章,网站就会通知到我们他们发布了文章。我们便可以第一时间了解到,但是看与不看取决于我们自己。

上述就是一个简单的发布订阅模式。up主就是发布者,我们(用户)就是订阅者,然后网站就是调度中心。

image.png

  • 发布者(Publisher):发布者通过调度中心发布事件(也就是up主通过网站来发布文章)
  • 订阅者(subscriber):通过调度中心订阅事件(也就是我们通过网站来接收通知)
  • 调用中心(Event Channel):负责存放订阅者和事件的关系(也就是网站这个平台)

如何实现一个简单的发布订阅模式?

我们先理一下整体的发布-订阅模式的思路:

  • 创建一个类class
  • 在这个类里创建一个缓存列表(调度中心)
  • on方法:把fn函数添加到缓存列表(订阅者注册事件到订阅中心)
  • emit方法:取到event事件类型(type),然后执行缓存列表下对应类型的函数(发布者发布事件到调度中心,调度中心处理代码)
  • off方法:可以根据event事件类型取消订阅(取消订阅)

我们下面根据思路来实现

1.创建一个EventEmitter类

我们先创建一个类,我们还需要一个构造函数如下:

class EventEmitter {
  constructor() {
    
  }
}

2.添加on、emit、off方法

我们为了把这三个方法长的像vue,每个方法前加一个$

class EventEmitter {
  constructor() {

  }
  // 向消息队列添加内容
  $on() {

  }
  // 出发消息队列的对应内容
  $emit() {

  }
  // 删除消息队列的对应内容
  $off() {

  }
}

我们先来创建一个订阅者

class EventEmitter {
  constructor() {

  }
  // 向消息队列添加内容
  $on() {

  }
  // 出发消息队列的对应内容
  $emit() {

  }
  // 删除消息队列的对应内容
  $off() {

  }
}
// 使用构造函数创建一个实例
const person1 = new EventEmitter()
// 像这个person1委托一些内容
person1.$on()

既然要委托一些内容,那事件名就必不可少,事件触发的时候还需要一个回调函数 所以需要两个参数:

  1. 事件名
  2. 回调函数

举个栗子

我们麻烦person1监听下买手机,手机到了之后去执行回调函数fn1fn2

class EventEmitter {
  constructor() {

  }
  // 向消息队列添加内容
  $on() {

  }
  // 触发消息队列的对应内容
  $emit() {

  }
  // 删除消息队列的对应内容
  $off() {

  }
}

function fn1() {
  console.log("我是函数fn1")
}

function fn2() {
  console.log("我是函数fn2")
}
// 使用构造函数创建一个实例
const person1 = new EventEmitter()
// 像这个person1委托一些内容,其中买手机是事件名,fn1和fn2是需要触发的回调函数
person1.$on('buyPhone', fn1)
person1.$on('buyPhone', fn2)

那我们怎么把这些事件存储起来呢,下面来看缓存列表(调度中心)

3.缓存列表

缓存列表message主要做什么: 向person1委托一个买手机之后,随后调用fn1fn2函数。 所以我们希望通过$on来实现,给message添加一个buyPhone属性后,然后这个属性的值为[fn1, fn2],如下

class EventEmitter {
  constructor() {
    this.message = {
    buyPhone: [fn1, fn2]
    } // 消息队列(缓存列表)
  }
  // 向消息队列添加内容
  $on() {

  }
  // 触发消息队列的对应内容
  $emit() {

  }
  // 删除消息队列的对应内容
  $off() {

  }
}

function fn1() {
  console.log("我是函数fn1")
}

function fn2() {
  console.log("我是函数fn2")
}
// 使用构造函数创建一个实例
const person1 = new EventEmitter()
// 像这个person1委托一些内容,其中买手机是事件名,fn1和fn2是需要触发的回调函数
person1.$on('buyPhone', fn1)
person1.$on('buyPhone', fn2)

那我们通过$on怎么实现呢?

4.$on方法的实现

person1.$on('buyPhone', fn1)

我们在确定下$on方法的实现思路:

  • 我们首先需要两个参数,一个type(事件名), 一个callback(回调函数)。
  • 判断缓存队列里是否存在type,如果不存在,就初始化一个空数组
  • 把回调函数push进缓存列表(调度中心)中(因为需要的回调函数可以是多个,所以是个数组) 下面我们来看下实现代码

event.js

class EventEmitter {
  constructor() {
    this.message = {} // 消息队列(缓存列表)

  }

  /** $on:向消息队列添加内容
   * @params {*} type:事件名
   * @params {*} callback:回调函数
   **/
  // 
  $on(type, callback) {
    // 判断是否有事件类型这个属性
    if (!this.message[type]) {
      // 没有的话,初始化一个空数组
      this.message[type] = [];
    }
    // 把回调函数放入调度中心中
    this.message[type].push(callback)
  }
  // 触发消息队列的对应内容
  $emit() {

  }
  // 删除消息队列的对应内容
  $off() {

  }
}

function fn1() {
  console.log("我是函数fn1")
}

function fn2() {
  console.log("我是函数fn2")
}
// 使用构造函数创建一个实例
const person1 = new EventEmitter()
// 像这个person1委托一些内容
person1.$on('buyPhone', fn1)
person1.$on('buyPhone', fn2)
console.log("person1====", person1)

我们加入一个html测试一下

demo.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
   <script src="./event.js"></script>
  </body>
</html>

运行结果如下

image.png

打印出来的是EventEmitter类,里面有个缓存列表,有个buyPhone的属性,对应的值是fn1fn2两个回调函数。说明测试通过了

5.$off方法

我们来看取消订阅的调用,可能会有两种:

  • 第一种person1.$off("buyPhone"):删除整个事件类型
  • 第二种person1.$off("buyPhone", fn1): 删除fn1消息,缓存列表里面其他消息不动

知道调用了,我们先来理一下思路

  • 首先我们有两个参数,一个type(事件类型),一个callback(回调函数)
  • 判断是不是有type属性,如果没有,直接return
  • 判断是不是有回调函数,如果没有回调函数,直接删除整个事件
  • 最后删除取消订阅的消息

实现代码如下

class EventEmitter {
  constructor() {
    this.message = {} // 消息队列(缓存列表)

  }

  /** $on:向消息队列添加内容
   * @params {*} type:事件名
   * @params {*} callback:回调函数
   **/
  $on(type, callback) {
    // 判断是否有事件类型这个属性
    if (!this.message[type]) {
      // 没有的话,初始化一个空数组
      this.message[type] = [];
    }
    // 把回调函数放入调度中心中
    this.message[type].push(callback)
  }
  // 触发消息队列的对应内容
  $emit() {

  }
  /** $off:删除消息队列的对应内容
   * @params {*} type:事件名
   * @params {*} callback:回调函数
   **/
  $off(type, callback) {
    // 判断如果没有type这个事件类型,没有直接return
    if (!this.message[type]) return;
    // 判断有没有回调函数,没有直接删除整个事件
    if (!callback) {
      this.message[type] = undefined
    }
    // 如果有callback,就仅仅删除callback这个消息
    this.message[type] = this.message[type].filter(item => item !== callback)

  }
}

function fn1() {
  console.log("我是函数fn1")
}

function fn2() {
  console.log("我是函数fn2")
}
// 使用构造函数创建一个实例
const person1 = new EventEmitter()
// 像这个person1委托一些内容
person1.$on('buyPhone', fn1)
person1.$on('buyPhone', fn2)
person1.$off('buyPhone', fn2)

console.log("person1====", person1)

我们再测试下,结果如下

image.png

person1.$off('buyPhone')
console.log("person1====", person1)

image.png

以上结果说明测试通过

这里说一个题外话:为什么删除对象的属性用object.key=undefined而不用delete直接删除,这里就有一个性能优化的问题存在。如果有兴趣,我可以下一篇说下这两者之间的区别

6.$emit方法

person1.$emit("buyPhone")

思路如下:

  • $emit需要传入一个参数type,用来确定需要触发哪个事件
  • 执行缓存列表里面的回调函数,需要对这个列表做一个循环,然后执行回调函数
class EventEmitter {
  constructor() {
    this.message = {} // 消息队列(缓存列表)

  }

  /** $on:向消息队列添加内容
   * @params {*} type:事件名
   * @params {*} callback:回调函数
   **/
  $on(type, callback) {
    // 判断是否有事件类型这个属性
    if (!this.message[type]) {
      // 没有的话,初始化一个空数组
      this.message[type] = [];
    }
    // 把回调函数放入调度中心中
    this.message[type].push(callback)
  }
  /** $emit:触发发消息队列的对应内容
   * @params {*} type:事件名
   **/
  $emit(type) {
    // 判断是否有订阅
    if (!this.message[type]) return;
    // 循环,然后执行缓存列表中对应事件类型下的所有回调函数
    this.message[type].forEach(item => {
      item();
    })
    

  }
  /** $off:删除消息队列的对应内容
   * @params {*} type:事件名
   * @params {*} callback:回调函数
   **/
  $off(type, callback) {
    // 判断如果没有type这个事件类型,没有直接return
    if (!this.message[type]) return;
    // 判断有没有回调函数,没有直接删除整个事件
    if (!callback) {
      this.message[type] = undefined
    }
    // 如果有callback,就仅仅删除callback这个消息
    this.message[type] = this.message[type].filter(item => item !== callback)

  }
}

function fn1() {
  console.log("我是函数fn1")
}

function fn2() {
  console.log("我是函数fn2")
}
// 使用构造函数创建一个实例
const person1 = new EventEmitter()
// 像这个person1委托一些内容
person1.$on('buyPhone', fn1)
person1.$on('buyPhone', fn2)
person1.$emit('buyPhone')

console.log("person1====", person1)

打印出来结果如下

image.png

发现成功的触发了,哈哈哈哈简单功能基本完成了

完整代码

参考文章

juejin.cn/post/705263…