likes
comments
collection

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

作者站长头像
站长
· 阅读数 17

前言

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

  • 订阅者(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. 添加三个核心方法

还需要添加三个方法,也就是我们前面讲到的onemitoff方法,为了让这个方法长得更像 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 监听一下 买红宝石 ,红宝石到了之后,执行回调函数 handlerAhandlerB,就可以这样写:

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类型的内容,完成之后执行回调函数 handlerAhandlerB

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]
}

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

  1. 先判断有没有这个属性(事件类型)
  2. 如果没有这个属性,就初始化一个空的数组
  3. 如果有这个属性,就往他的后面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 方法有两种写法:

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

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

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 删除对象的某个属性 有两种方法:

  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就👌了。

具体代码实现如下:

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')

输出结果:

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

测试通过 👏👏👏

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

完整代码

本篇文章实现了最简单的发布订阅者模式,他的核心内容只有四个:

  1. 缓存列表 message
  2. 向消息队列添加内容 $on
  3. 删除消息队列里的内容 $off
  4. 触发消息队列里的内容 $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()
        });
    }
}

❤️ 结尾

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

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

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

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

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