从零带你手写一个“发布-订阅者模式“ ,保姆级教学
前言
发布-订阅模式其实是一种对象间一对多的依赖关系,当一个对象的状态发送改变时,所有依赖于它的对象都将得到状态改变的通知。
- 订阅者(Subscriber)把自己想订阅的事件 注册(Subscribe)到调度中心(Event Channel);
- 当发布者(Publisher)发布该事件(Publish Event)到调度中心,也就是该事件触发时,由 调度中心 统一调度(Fire Event)订阅者注册到调度中心的处理代码。
◾ 例子
比如我们很喜欢看某个公众号的文章,但是不知道什么时候发布新文章,要不定时的去翻阅;这时候,我们可以关注该公众号,当有文章推送时,会有消息及时通知我们文章更新了。
上面一个看似简单的操作,其实是一个典型的发布订阅模式,公众号属于发布者,用户属于订阅者;用户将订阅公众号的事件注册到调度中心,公众号作为发布者,当有新文章发布时,公众号发布该事件到调度中心,调度中心会及时发消息告知用户。
◾ 发布/订阅模式的优点是对象之间解耦,异步编程中,可以更松耦合的代码编写;缺点是创建订阅者本身要消耗一定的时间和内存,虽然可以弱化对象之间的联系,多个发布者和订阅者嵌套一起的时候,程序难以跟踪维护。
手写实现发布-订阅者模式
整体的发布-订阅者模式实现思路如下:
- 创建一个类 class
- 在这个类里创建一个缓存列表(调度中心)
on
方法 - 用来把函数fn
添加到缓存列表(订阅者注册事件到调度中心)emit
方法 - 取到event
事件类型,根据event
值去执行对应缓存列表中的函数(发布者发布事件到调度中心,调度中心处理代码)off
方法 - 可以根据event
事件类型取消订阅(取消订阅)
接下来我们根据上面的思路,开始手写发布-订阅者模式 👇
1. 创建一个 Observer 类
我们先创建一个 Ovserver 类:
+ class Observer {
+
+ }
在 Observer 类里,需要添加一个构造函数:
class Observer {
+ constructor(){
+
+ }
}
2. 添加三个核心方法
还需要添加三个方法,也就是我们前面讲到的on
、emit
和off
方法,为了让这个方法长得更像 Vue,我们在这几个方法前面都加上$
,即:
- 向消息队列添加内容
$on
- 删除消息队列里的内容
$off
- 触发消息队列里的内容
$emit
class Observer {
constructor() {
}
+ // 向消息队列添加内容 `$on`
+ $on(){}
+ // 删除消息队列里的内容 `$off`
+ $off(){}
+ // 触发消息队列里的内容 `$emit`
+ $emit(){}
}
方法具体的内容我们放一放,先来创建一个订阅者(发布者),
使用构造函数创建一个实例:
class Observer {
constructor() {
}
// 向消息队列添加内容 `$on`
$on(){}
// 删除消息队列里的内容 `$off`
$off(){}
// 触发消息队列里的内容 `$emit`
$emit(){}
}
+ // 使用构造函数创建一个实例
+ const person1 = new Observer()
接着,我们向这个person1
委托一些内容,也就是说调用person1
的$ON
方法:
class Observer {
constructor() {
}
// 向消息队列添加内容 `$on`
$on() {}
// 删除消息队列里的内容 `$off`
$off() {}
// 触发消息队列里的内容 `$emit`
$emit() {}
}
// 使用构造函数创建一个实例
const person1 = new Observer();
+ // 向这个`person1`委托一些内容,调用`person1 `的`$ON`方法
+ person1.$on()
既然要委托一些内容,那 事件名 就必不可少,事件触发的时候也需要一个 回调函数
- 事件名
- 回调函数
举个例子,我们写几个事件,比如:
class Observer {
constructor() {
}
// 向消息队列添加内容 `$on`
$on() {}
// 删除消息队列里的内容 `$off`
$off() {}
// 触发消息队列里的内容 `$emit`
$emit() {}
}
// 使用构造函数创建一个实例
const person1 = new Observer();
// 向这个`person1`委托一些内容,调用`person1 `的`$ON`方法
person1.$on()
+ function handlerA() {
+ console.log('handlerA');
+ }
+
+ function handlerB() {
+ console.log('handlerB');
+ }
+
+ function handlerC() {
+ console.log('handlerC');
}
我们现在拜托 person1
监听一下 买红宝石
,红宝石到了之后,执行回调函数 handlerA
和 handlerB
,就可以这样写:
class Observer {
constructor() {
}
// 向消息队列添加内容 `$on`
$on() {}
// 删除消息队列里的内容 `$off`
$off() {}
// 触发消息队列里的内容 `$emit`
$emit() {}
}
// 使用构造函数创建一个实例
const person1 = new Observer();
// 向这个`person1`委托一些内容,调用`person1 `的`$ON`方法
+ person1.$on('买红宝石', handlerA)
+ person1.$on('买红宝石', handlerB)
function handlerA() {
console.log('handlerA');
}
function handlerB() {
console.log('handlerB');
}
function handlerC() {
console.log('handlerC');
}
再拜托 person1
监听一下 买奶茶
,奶茶到了之后,执行回调函数 handlerC
:
class Observer {
constructor() {
}
// 向消息队列添加内容 `$on`
$on() {}
// 删除消息队列里的内容 `$off`
$off() {}
// 触发消息队列里的内容 `$emit`
$emit() {}
}
// 使用构造函数创建一个实例
const person1 = new Observer();
// 向这个`person1`委托一些内容,调用`person1 `的`$ON`方法
person1.$on('买红宝石', handlerA)
person1.$on('买红宝石', handlerB)
+ person1.$on('买奶茶', handlerC)
function handlerA() {
console.log('handlerA');
}
function handlerB() {
console.log('handlerB');
}
function handlerC() {
console.log('handlerC');
}
3. 设置缓存列表
到这里我们就需要前面讲到的 缓存列表(消息队列),也就是调度中心了。
给Observer
类添加 缓存列表:
class Observer {
constructor() {
+ this.message = {} // 消息队列
}
// 向消息队列添加内容 `$on`
$on() {}
// 删除消息队列里的内容 `$off`
$off() {}
// 触发消息队列里的内容 `$emit`
$emit() {}
}
// 使用构造函数创建一个实例
const person1 = new Observer();
这个缓存列表 message
对象的功能如下:
向 person1
委托一个buy
类型的内容,完成之后执行回调函数 handlerA
和 handlerB
class Observer {
constructor() {
this.message = {} // 消息队列
}
// 向消息队列添加内容 `$on`
$on() {}
// 删除消息队列里的内容 `$off`
$off() {}
// 触发消息队列里的内容 `$emit`
$emit() {}
}
// 使用构造函数创建一个实例
const person1 = new Observer();
+ person1.$on('buy',handlerA);
+ person1.$on('buy',handlerB);
function handlerA() {
console.log('handlerA');
}
function handlerB() {
console.log('handlerB');
}
function handlerC() {
console.log('handlerC');
}
我们希望通过$on
向消息队列添加上面内容后,就相当对给message
对象添加了一个buy
属性,这个属性值为[handlerA, handlerB]
,相当于下面的效果:
class Observer {
constructor() {
this.message = {
+ buy: [handlerA, handlerB]
}
}
// 向消息队列添加内容 `$on`
$on() {}
// 删除消息队列里的内容 `$off`
$off() {}
// 触发消息队列里的内容 `$emit`
$emit() {}
}
需求明确后,下面着手 写 $on
函数 👇👇👇
4. 实现 $on 方法
回顾一行代码:
person1.$on('buy',handlerA);
很明显我们给$on
方法传入了两个参数:
type
:事件名 (事件类型)callback
:回调函数
class Observer {
constructor() {
this.message = {} // 消息队列
}
+ /**
+ * `$on` 向消息队列添加内容
+ * @param {*} type 事件名 (事件类型)
+ * @param {*} callback 回调函数
+ */
+ $on(type, callback) {}
// 删除消息队列里的内容 `$off`
$off() {}
// 触发消息队列里的内容 `$emit`
$emit() {}
}
// 使用构造函数创建一个实例
const person1 = new Observer();
person1.$on('buy', handlerA);
person1.$on('buy', handlerB);
我们初步设想一下如何向消息队列添加内容,消息队列是一个对象,可以通过下面的方法添加事件内容:
class Observer {
constructor() {
this.message = {} // 消息队列
}
/**
* `$on` 向消息队列添加内容
* @param {*} type 事件名 (事件类型)
* @param {*} callback 回调函数
*/
$on(type, callback) {
+ this.message[type] = callback;
}
// 删除消息队列里的内容 `$off`
$off() {}
// 触发消息队列里的内容 `$emit`
$emit() {}
}
但通过前文我们知道消息队列中每个属性值都是 数组:
this.message = {
buy: [handlerA, handlerB]
}
即每个事件类型对应多个消息 (回调函数),这样的话我们就要为每个事件类型创建一个数组,具体写法:
- 先判断有没有这个属性(事件类型)
- 如果没有这个属性,就初始化一个空的数组
- 如果有这个属性,就往他的后面push一个新的 callback
代码实现如下:
class Observer {
constructor() {
this.message = {} // 消息队列
}
/**
* `$on` 向消息队列添加内容
* @param {*} type 事件名 (事件类型)
* @param {*} callback 回调函数
*/
$on(type, callback) {
+ // 判断有没有这个属性(事件类型)
+ if (!this.message[type]) {
+ // 如果没有这个属性,就初始化一个空的数组
+ this.message[type] = [];
+ }
+ // 如果有这个属性,就往他的后面push一个新的callback
+ this.message[type].push(callback)
}
// 删除消息队列里的内容 `$off`
$off() {}
// 触发消息队列里的内容 `$emit`
$emit() {}
}
$on
的代码实现如上所示,我们加上用例并引入到一个html文件中测试一下:
Observe.js
class Observer {
constructor() {
this.message = {} // 消息队列
}
/**
* `$on` 向消息队列添加内容
* @param {*} type 事件名 (事件类型)
* @param {*} callback 回调函数
*/
$on(type, callback) {
// 判断有没有这个属性(事件类型)
if (!this.message[type]) {
// 如果没有这个属性,就初始化一个空的数组
this.message[type] = [];
}
// 如果有这个属性,就往他的后面push一个新的callback
this.message[type].push(callback)
}
// 删除消息队列里的内容 `$off`
$off() {}
// 触发消息队列里的内容 `$emit`
$emit() {}
}
function handlerA() {
console.log('handlerA');
}
function handlerB() {
console.log('handlerB');
}
function handlerC() {
console.log('handlerC');
}
// 使用构造函数创建一个实例
const person1 = new Observer();
person1.$on('buy', handlerA);
person1.$on('buy', handlerB);
console.log('person1 :>> ', person1);
Oberver.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>
<!-- 引入Observe.js文件 -->
<script src="../JS/Observer.js"></script>
</body>
</html>
输出结果:
打印出的 person1 是 Oberver 类型的,里面有一个message,也就是咱定义的消息队列;这个message里有我们添加的buy
类型的事件,这个buy
事件有两个消息:[handlerA,handlerB]
,测试通过 👏👏👏
接下来,我们来实现 $off
方法
5. 实现 $off 方法
$off
方法用来删除消息队列里的内容
$off
方法有两种写法:
person1.$off("buy")
- 删除整个buy
事件类型person1.$off("buy",handlerA)
- 只删除handlerA
消息,保留buy
事件列表里的其他消息
和$on
方法一样,$off
方法也需要type
和callback
这两个方法:
class Observer {
constructor() {
this.message = {} // 消息队列
}
/**
* `$on` 向消息队列添加内容
* @param {*} type 事件名 (事件类型)
* @param {*} callback 回调函数
*/
$on(type, callback) {
// 判断有没有这个属性(事件类型)
if (!this.message[type]) {
// 如果没有这个属性,就初始化一个空的数组
this.message[type] = [];
}
// 如果有这个属性,就往他的后面push一个新的callback
this.message[type].push(callback)
}
+ /**
+ * $off 删除消息队列里的内容
+ * @param {*} type 事件名 (事件类型)
+ * @param {*} callback 回调函数
+ */
+ $off(type, callback) {}
// 触发消息队列里的内容 `$emit`
$emit() {}
}
$off
方法的实现步骤如下:
- 判断是否有订阅,即消息队列里是否有type这个类型的事件,没有的话就直接return
- 判断是否有fn这个参数
- 没有fn就删掉整个事件
- 有fn就仅仅删掉fn这个消息
代码实现如下:
class Observer {
constructor() {
this.message = {} // 消息队列
}
/**
* `$on` 向消息队列添加内容
* @param {*} type 事件名 (事件类型)
* @param {*} callback 回调函数
*/
$on(type, callback) {
// 判断有没有这个属性(事件类型)
if (!this.message[type]) {
// 如果没有这个属性,就初始化一个空的数组
this.message[type] = [];
}
// 如果有这个属性,就往他的后面push一个新的callback
this.message[type].push(callback);
}
/**
* $off 删除消息队列里的内容
* @param {*} type 事件名 (事件类型)
* @param {*} callback 回调函数
*/
$off(type, callback) {
+ // 判断是否有订阅,即消息队列里是否有type这个类型的事件,没有的话就直接return
+ if (!this.message[type]) return;
+ // 判断是否有callback这个参数
+ if (!callback) {
+ // 如果没有callback,就删掉整个事件ß
+ this.message[type] = undefined;
+ }
+ // 如果有callback,就仅仅删掉callback这个消息(过滤掉这个消息方法)
+ this.message[type] = this.message[type].filter((item) => item !== callback);
}
// 触发消息队列里的内容 `$emit`
$emit() {}
}
以上就是$off
的实现,我们先来测试一下:
class Observer {
...
}
function handlerA() {
console.log('handlerA');
}
function handlerB() {
console.log('handlerB');
}
function handlerC() {
console.log('handlerC');
}
// 使用构造函数创建一个实例
const person1 = new Observer();
person1.$on('buy', handlerA);
person1.$on('buy', handlerB);
person1.$on('buy', handlerC);
console.log('person1 :>> ', person1);
输出结果:
● 测试删除单个消息,使用$off
删除 handlerC 消息
class Observer {
...
}
function handlerA() {
console.log('handlerA');
}
function handlerB() {
console.log('handlerB');
}
function handlerC() {
console.log('handlerC');
}
// 使用构造函数创建一个实例
const person1 = new Observer();
person1.$on('buy', handlerA);
person1.$on('buy', handlerB);
person1.$on('buy', handlerC);
console.log('person1 :>> ', person1);
+ // 删除 handlerC 消息
+ person1.$off('buy',handlerC);
+ console.log('person1 :>> ', person1);
输出结果:
测试通过 🥳🥳🥳
● 测试删除整个事件类型,使用$off
删除整个 buy 事件
class Observer {
...
}
function handlerA() {
console.log('handlerA');
}
function handlerB() {
console.log('handlerB');
}
function handlerC() {
console.log('handlerC');
}
// 使用构造函数创建一个实例
const person1 = new Observer();
person1.$on('buy', handlerA);
person1.$on('buy', handlerB);
person1.$on('buy', handlerC);
console.log('person1 :>> ', person1);
// 删除 handlerC 消息
person1.$off('buy',handlerC);
console.log('person1 :>> ', person1);
+ // 删除 buy 事件
+ person1.$off('buy');
+ console.log('person1 :>> ', person1);
输出结果:
Perfect!!!测试通过 ✅
这样以来 $off
的两个功能我们就已经成功实现 👏👏👏
● 关于 $off
的实现,这里讲一个小细节 👇
javascript 删除对象的某个属性 有两种方法:
- delete 操作符
obj.key = undefined;
(等同于obj[key] = undefined;
)
这两种方法的区别:
1. delete 操作符会从某个对象上移除指定属性,但它的工作量比其“替代”设置也就是 object[key] = undefined
多的多的多。
并且该方法有诸多限制,比如,以下情况需要重点考虑:
-
如果你试图删除的属性不存在,那么delete将不会起任何作用,但仍会返回true
-
如果对象的原型链上有一个与待删除属性同名的属性,那么删除属性之后,对象会使用原型链上的那个属性(也就是说,delete操作只会在自身的属性上起作用)
-
任何使用
var
声明的属性不能从全局作用域或函数的作用域中删除。- 这样的话,delete操作不能删除任何在全局作用域中的函数(无论这个函数是来自于函数声明或函数表达式)
- 除了在全局作用域中的函数不能被删除,在对象(object)中的函数是能够用delete操作删除的。
-
任何用
let
或const
声明的属性不能够从它被声明的作用域中删除。 -
不可设置的(Non-configurable)属性不能被移除。这意味着像
Math
,Array
,Object
内置对象的属性以及使用Object.defineProperty()
方法设置为不可设置的属性不能被删除。
2. obj[key] = undefined;
这个选择不是这个问题的正确答案,因为只是把某个属性替换为undefined
,属性本身还在。但是,如果你小心使用它,你可以大大加快一些算法。
好了,回到正题 🙌
接下来我们开始实现第三个方法 $emit
🧗♀️
6. 实现 $emit
方法
$emit
用来触发消息队列里的内容:
-
该方法需要传入一个
type
参数,用来确定触发哪一个事件; -
主要流程就是对这个
type
事件做一个轮询 (for循环),挨个执行每一个消息的回调函数callback就👌了。
具体代码实现如下:
class Observer {
constructor() {
this.message = {} // 消息队列
}
/**
* `$on` 向消息队列添加内容
* @param {*} type 事件名 (事件类型)
* @param {*} callback 回调函数
*/
$on(type, callback) {
// 判断有没有这个属性(事件类型)
if (!this.message[type]) {
// 如果没有这个属性,就初始化一个空的数组
this.message[type] = [];
}
// 如果有这个属性,就往他的后面push一个新的callback
this.message[type].push(callback);
}
/**
* $off 删除消息队列里的内容
* @param {*} type 事件名 (事件类型)
* @param {*} callback 回调函数
*/
$off(type, callback) {
// 判断是否有订阅,即消息队列里是否有type这个类型的事件,没有的话就直接return
if (!this.message[type]) return;
// 判断是否有callback这个参数
if (!callback) {
// 如果没有callback,就删掉整个事件
this.message[type] = undefined;
return;
}
// 如果有callback,就仅仅删掉callback这个消息(过滤掉这个消息方法)
this.message[type] = this.message[type].filter((item) => item !== callback);
}
+ /**
+ * $emit 触发消息队列里的内容
+ * @param {*} type 事件名 (事件类型)
+ */
+ $emit(type) {
+ // 判断是否有订阅
+ if(!this.message[type]) return;
+ // 如果有订阅,就对这个`type`事件做一个轮询 (for循环)
+ this.message[type].forEach(item => {
+ // 挨个执行每一个消息的回调函数callback
+ item()
+ });
+ }
}
打完收工🏌️♀️
来测试一下吧~
class Observer {
...
}
function handlerA() {
console.log('buy handlerA');
}
function handlerB() {
console.log('buy handlerB');
}
function handlerC() {
console.log('buy handlerC');
}
// 使用构造函数创建一个实例
const person1 = new Observer();
+ person1.$on('buy', handlerA);
+ person1.$on('buy', handlerB);
+ person1.$on('buy', handlerC);
console.log('person1 :>> ', person1);
+ // 触发 buy 事件
+ person1.$emit('buy')
输出结果:
测试通过 👏👏👏
完整代码
本篇文章实现了最简单的发布订阅者模式,他的核心内容只有四个:
- 缓存列表
message
- 向消息队列添加内容
$on
- 删除消息队列里的内容
$off
- 触发消息队列里的内容
$emit
发布订阅者模式完整代码实现:
完整版的代码较长,这里看着如果不方便的可以去我的GitHub上看,我专门维护了一个 前端 BLOG 的仓库:github.com/yuanyuanbyt…
class Observer {
constructor() {
this.message = {} // 消息队列
}
/**
* `$on` 向消息队列添加内容
* @param {*} type 事件名 (事件类型)
* @param {*} callback 回调函数
*/
$on(type, callback) {
// 判断有没有这个属性(事件类型)
if (!this.message[type]) {
// 如果没有这个属性,就初始化一个空的数组
this.message[type] = [];
}
// 如果有这个属性,就往他的后面push一个新的callback
this.message[type].push(callback);
}
/**
* $off 删除消息队列里的内容
* @param {*} type 事件名 (事件类型)
* @param {*} callback 回调函数
*/
$off(type, callback) {
// 判断是否有订阅,即消息队列里是否有type这个类型的事件,没有的话就直接return
if (!this.message[type]) return;
// 判断是否有callback这个参数
if (!callback) {
// 如果没有callback,就删掉整个事件
this.message[type] = undefined;
return;
}
// 如果有callback,就仅仅删掉callback这个消息(过滤掉这个消息方法)
this.message[type] = this.message[type].filter((item) => item !== callback);
}
/**
* $emit 触发消息队列里的内容
* @param {*} type 事件名 (事件类型)
*/
$emit(type) {
// 判断是否有订阅
if(!this.message[type]) return;
// 如果有订阅,就对这个`type`事件做一个轮询 (for循环)
this.message[type].forEach(item => {
// 挨个执行每一个消息的回调函数callback
item()
});
}
}
❤️ 结尾
如果这篇文章 对你的学习 有所 帮助,欢迎 点赞 👍 收藏 ⭐ 留言 📝 ,你的支持 是我 创作分享 的 动力!
学习过程中如果有疑问,点击这里,可以获得我的联系方式,与我交流~
关注公众号「前端圆圆」,第一时间获取文章更新。
更多更全更详细 的 优质内容, 猛戳这里查看
转载自:https://juejin.cn/post/7052637219084828680