yuanyuanbyte / Blog

圆圆的博客,预计写七个系列:JavaScript深入系列、JavaScript专题系列、网络系列、Webpack系列、Vue系列、JavaScript基础系列、HTML&CSS应知应会系列。
306 stars 125 forks source link

JavaScript 深入系列之发布-订阅者模式的模拟实现 #127

Open yuanyuanbyte opened 2 years ago

yuanyuanbyte commented 2 years ago

本系列的主题是 JavaScript 深入系列,每期讲解一个技术要点。如果你还不了解各系列内容,文末点击查看全部文章,点我跳转到文末

如果觉得本系列不错,欢迎 Star,你的支持是我创作分享的最大动力。

前言

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

◾ 例子

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

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

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

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

整体的发布-订阅者模式实现思路如下:

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

1. 创建一个 Observer 类

我们先创建一个 Ovserver 类:

+ class Observer {
+     
+ }

在 Observer 类里,需要添加一个构造函数:

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

2. 添加三个核心方法

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

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方法传入了两个参数:

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方法的实现步骤如下:

代码实现如下:

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 多的多的多。

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

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

好了,回到正题 🙌

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

6. 实现 $emit 方法

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

具体代码实现如下:

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 的仓库https://github.com/yuanyuanbyte/Blog

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://github.com/yuanyuanbyte/Blog

我是圆圆,一名深耕于前端开发的攻城狮。

weixin

xiaomoumou commented 1 year ago

// 判断是否有callback这个参数

你好,这里应该还需要一个 return 吧