虽然发布订阅模式严格意义来讲不属于设计模式中的一种,但是在各大框架源码中,经常使用发布订阅模式。下面我们来看下怎么实现的吧。
什么是发布订阅模式?
发布订阅模式是一种对象间一对多的依赖关系,当一个对象的状态(发布者)发生改变时,所有依赖它的对象(订阅者)都将得到通知。
举个栗子
当我们在浏览博客论坛之类的网站时,遇到感兴趣的up主,我们会订阅他们的文章,这样一来,他们每次在网站发布一个文章,网站就会通知到我们他们发布了文章。我们便可以第一时间了解到,但是看与不看取决于我们自己。
上述就是一个简单的发布订阅模式。up主就是发布者,我们(用户)就是订阅者,然后网站就是调度中心。
发布者(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()
既然要委托一些内容,那事件名就必不可少,事件触发的时候还需要一个回调函数 所以需要两个参数:
- 事件名
- 回调函数
举个栗子
我们麻烦person1
监听下买手机,手机到了之后去执行回调函数fn1
和fn2
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
委托一个买手机之后,随后调用fn1
和fn2
函数。
所以我们希望通过$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>
运行结果如下
打印出来的是EventEmitter
类,里面有个缓存列表,有个buyPhone
的属性,对应的值是fn1
和fn2
两个回调函数。说明测试通过了
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)
我们再测试下,结果如下
person1.$off('buyPhone')
console.log("person1====", person1)
以上结果说明测试通过
这里说一个题外话:为什么删除对象的属性用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)
打印出来结果如下
发现成功的触发了,哈哈哈哈简单功能基本完成了
参考文章