从零带你手写一个“发布-订阅者模式“ ,保姆级教学

语言: CN / TW / HK
ead>

前言

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

  • 订阅者(Subscriber)把自己想订阅的事件 注册(Subscribe)到调度中心(Event Channel);
  • 发布者(Publisher)发布该事件(Publish Event)到调度中心,也就是该事件触发时,由 调度中心 统一调度(Fire Event)订阅者注册到调度中心的处理代码。

◾ 例子

比如我们很喜欢看某个公众号的文章,但是不知道什么时候发布新文章,要不定时的去翻阅;这时候,我们可以关注该公众号,当有文章推送时,会有消息及时通知我们文章更新了。

上面一个看似简单的操作,其实是一个典型的发布订阅模式,公众号属于发布者,用户属于订阅者;用户将订阅公众号的事件注册到调度中心,公众号作为发布者,当有新文章发布时,公众号发布该事件到调度中心,调度中心会及时发消息告知用户。

◾ 发布/订阅模式的优点是对象之间解耦,异步编程中,可以更松耦合的代码编写;缺点是创建订阅者本身要消耗一定的时间和内存,虽然可以弱化对象之间的联系,多个发布者和订阅者嵌套一起的时候,程序难以跟踪维护。

手写实现发布-订阅者模式

整体的发布-订阅者模式实现思路如下: - 创建一个类 class - 在这个类里创建一个缓存列表(调度中心) - on 方法 - 用来把函数fn添加到缓存列表(订阅者注册事件到调度中心) - emit 方法 - 取到event事件类型,根据event值去执行对应缓存列表中的函数(发布者发布事件到调度中心,调度中心处理代码) - off 方法 - 可以根据event事件类型取消订阅(取消订阅)

接下来我们根据上面的思路,开始手写发布-订阅者模式 👇

1. 创建一个 Observer 类

我们先创建一个 Ovserver 类: javascript + class Observer { + + } 在 Observer 类里,需要添加一个构造函数:

javascript class Observer { + constructor(){ + + } }

2. 添加三个核心方法

还需要添加三个方法,也就是我们前面讲到的onemitoff方法,为了让这个方法长得更像 Vue,我们在这几个方法前面都加上$,即:

  • 向消息队列添加内容 $on
  • 删除消息队列里的内容 $off
  • 触发消息队列里的内容 $emit

```javascript class Observer { constructor() {

}
  • // 向消息队列添加内容 $on
  • $on(){}
  • // 删除消息队列里的内容 $off
  • $off(){}
  • // 触发消息队列里的内容 $emit
  • $emit(){} } ```

方法具体的内容我们放一放,先来创建一个订阅者(发布者),

使用构造函数创建一个实例:

```javascript class Observer { constructor() {

}
// 向消息队列添加内容 `$on`
$on(){}
// 删除消息队列里的内容 `$off`
$off(){}
// 触发消息队列里的内容 `$emit`
$emit(){}

}

  • // 使用构造函数创建一个实例
  • const person1 = new Observer() `` 接着,我们向这个person1委托一些内容,也就是说调用person1 $ON`方法:

```javascript class Observer { constructor() {

}
// 向消息队列添加内容 `$on`
$on() {}
// 删除消息队列里的内容 `$off`
$off() {}
// 触发消息队列里的内容 `$emit`
$emit() {}

}

// 使用构造函数创建一个实例 const person1 = new Observer();

  • // 向这个person1委托一些内容,调用person1$ON方法
  • person1.$on() ``` 既然要委托一些内容,那 事件名 就必不可少,事件触发的时候也需要一个 回调函数

  • 事件名

  • 回调函数

举个例子,我们写几个事件,比如:

```javascript 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监听一下买红宝石,红宝石到了之后,执行回调函数handlerAhandlerB`,就可以这样写:

```javascript 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 :

```javascript 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类添加 缓存列表:

``javascript class Observer { constructor() { + this.message = {} // 消息队列 } // 向消息队列添加内容$on$on() {} // 删除消息队列里的内容$off$off() {} // 触发消息队列里的内容$emit` $emit() {} }

// 使用构造函数创建一个实例 const person1 = new Observer(); `` 这个缓存列表message` 对象的功能如下:

person1 委托一个buy类型的内容,完成之后执行回调函数 handlerAhandlerB

``javascript 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]`,相当于下面的效果:

javascript class Observer { constructor() { this.message = { + buy: [handlerA, handlerB] } } // 向消息队列添加内容 `$on` $on() {} // 删除消息队列里的内容 `$off` $off() {} // 触发消息队列里的内容 `$emit` $emit() {} } 需求明确后,下面着手 $on 函数 👇👇👇

4. 实现 $on 方法

回顾一行代码:

javascript person1.$on('buy',handlerA); 很明显我们给$on方法传入了两个参数:

  • type:事件名 (事件类型)
  • callback:回调函数

```javascript 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); ``` 我们初步设想一下如何向消息队列添加内容,消息队列是一个对象,可以通过下面的方法添加事件内容:

```js class Observer { constructor() { this.message = {} // 消息队列 }

/**
 * `$on` 向消息队列添加内容 
 * @param {*} type 事件名 (事件类型)
 * @param {*} callback 回调函数
 */
$on(type, callback) {
  • this.message[type] = callback; } // 删除消息队列里的内容 $off $off() {} // 触发消息队列里的内容 $emit $emit() {} } ``` 但通过前文我们知道消息队列中每个属性值都是 数组

js this.message = { buy: [handlerA, handlerB] }

即每个事件类型对应多个消息 (回调函数),这样的话我们就要为每个事件类型创建一个数组,具体写法:

  1. 先判断有没有这个属性(事件类型)
  2. 如果没有这个属性,就初始化一个空的数组
  3. 如果有这个属性,就往他的后面push一个新的 callback

代码实现如下:

```javascript 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

```javascript 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

```html

Document

``` 输出结果:

在这里插入图片描述

打印出的 person1 是 Oberver 类型的,里面有一个message,也就是咱定义的消息队列;这个message里有我们添加的buy类型的事件,这个buy事件有两个消息:[handlerA,handlerB],测试通过 👏👏👏

接下来,我们来实现 $off 方法

5. 实现 $off 方法

$off 方法用来删除消息队列里的内容

$off 方法有两种写法: 1. person1.$off("buy") - 删除整个buy事件类型 2. person1.$off("buy",handlerA) - 只删除handlerA消息,保留buy事件列表里的其他消息

$on方法一样,$off方法也需要typecallback这两个方法:

```javascript 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这个消息

代码实现如下:

```javascript 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`的实现,我们先来测试一下:

```javascript 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 消息

```javascript 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 事件

```javascript 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 删除对象的某个属性 有两种方法: 1. delete 操作符 2. obj.key = undefined; (等同于obj[key] = undefined;)

这两种方法的区别:

1. delete 操作符会从某个对象上移除指定属性,但它的工作量比其“替代”设置也就是 object[key] = undefined 多的多的多。

并且该方法有诸多限制,比如,以下情况需要重点考虑:

  • 如果你试图删除的属性不存在,那么delete将不会起任何作用,但仍会返回true

  • 如果对象的原型链上有一个与待删除属性同名的属性,那么删除属性之后,对象会使用原型链上的那个属性(也就是说,delete操作只会在自身的属性上起作用)

  • 任何使用 var 声明的属性不能从全局作用域或函数的作用域中删除。

    • 这样的话,delete操作不能删除任何在全局作用域中的函数(无论这个函数是来自于函数声明或函数表达式)
    • 除了在全局作用域中的函数不能被删除,在对象(object)中的函数是能够用delete操作删除的。
  • 任何用letconst 声明的属性不能够从它被声明的作用域中删除。

  • 不可设置的(Non-configurable)属性不能被移除。这意味着像Math, ArrayObject内置对象的属性以及使用Object.defineProperty()方法设置为不可设置的属性不能被删除。

2. obj[key] = undefined; 这个选择不是这个问题的正确答案,因为只是把某个属性替换为undefined,属性本身还在。但是,如果你小心使用它,你可以大大加快一些算法。

好了,回到正题 🙌

接下来我们开始实现第三个方法 $emit 🧗‍♀️

6. 实现 $emit 方法

$emit 用来触发消息队列里的内容:

  • 该方法需要传入一个 type 参数,用来确定触发哪一个事件;

  • 主要流程就是对这个type事件做一个轮询 (for循环),挨个执行每一个消息的回调函数callback就👌了。

具体代码实现如下:

```javascript 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()
  • });
  • } } ``` 打完收工🏌️‍♀️

来测试一下吧~

```javascript 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') ``` 输出结果:

在这里插入图片描述

测试通过 👏👏👏

在这里插入图片描述

完整代码

本篇文章实现了最简单的发布订阅者模式,他的核心内容只有四个: 1. 缓存列表 message 2. 向消息队列添加内容 $on 3. 删除消息队列里的内容 $off 4. 触发消息队列里的内容 $emit

发布订阅者模式完整代码实现:

完整版的代码较长,这里看着如果不方便的可以去我的GitHub上看,我专门维护了一个 前端 BLOG 的仓库https://github.com/yuanyuanbyte/Blog

```javascript 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()
    });
}

} ```

❤️ 结尾

如果这篇文章 对你的学习 有所 帮助,欢迎 点赞 👍 收藏留言 📝 ,你的支持 是我 创作分享动力!

学习过程中如果有疑问,点击这里,可以获得我的联系方式,与我交流~

关注公众号「前端圆圆」,第一时间获取文章更新。

更多更全更详细优质内容猛戳这里查看

在这里插入图片描述