发布订阅设计模式

发布订阅模式

二、手搓一个发布订阅事件中心

==============

“纸上得来终觉浅,绝知此事要躬行”,所以根据定义,我们尝试实现一个JavaScript版本的发布订阅事件中心,看看会遇到哪些问题?

2.1 基本结构版


首先实现的 DiyEventEmitter 如下:

/**

* 事件发布订阅中心

*/

class DiyEventEmitter {

static instance: DiyEventEmitter;

private _eventsMap: Map<string, Array<() => void>>;

static getInstance() {

if (!DiyEventEmitter.instance) {

DiyEventEmitter.instance = new DiyEventEmitter();

}

return DiyEventEmitter.instance;

}

constructor() {

this._eventsMap = new Map(); // 事件名与回调函数的映射Map

}

/**

* 事件订阅

* @param eventName 事件名

* @param eventFnCallback 事件发生时的回调函数

*/

public on(eventName: string, eventFnCallback: () => void) {

const newArr = this._eventsMap.get(eventName) || [];

newArr.push(eventFnCallback);

this._eventsMap.set(eventName, newArr);

}

/**

* 取消订阅

* @param eventName 事件名

* @param eventFnCallback 事件发生时的回调函数

*/

public off(eventName: string, eventFnCallback?: () => void) {

if (!eventFnCallback) {

this._eventsMap.delete(eventName);

return;

}

const newArr = this._eventsMap.get(eventName) || [];

for (let i = newArr.length - 1; i >= 0; i–) {

if (newArr[i] === eventFnCallback) {

newArr.splice(i, 1);

}

}

this._eventsMap.set(eventName, newArr);

}

/**

* 主动通知并执行注册的回调函数

* @param eventName 事件名

*/

public emit(eventName: string) {

const fns = this._eventsMap.get(eventName) || [];

fns.forEach(fn => fn());

}

}

export default DiyEventEmitter.getInstance();

导出的 DiyEventEmitter 是一个“单例”,保证在全局中只有唯一“事件中心”实例,使用时候直接可使用公共方法

import e from “./DiyEventEmitter”;

const subscribeFn = () => {

console.log(“DYBOY订阅收到了消息”);

};

const subscribeFn2 = () => {

console.log(“DYBOY第二个订阅收到了消息”);

};

// 订阅

e.on(“dyboy”, subscribeFn);

e.on(“dyboy”, subscribeFn2);

// 发布消息

e.emit(“dyboy”);

// 取消第一个订阅消息的绑定

e.off(“dyboy”, subscribeFn);

// 第二次发布消息

e.emit(“dyboy”);

输出 console 结果:

DYBOY订阅收到了消息

第二个订阅的消息

第二个订阅的消息

那么第一版的支持订阅、发布、取消的“发布订阅事件中心”就OK了。

2.2 支持只订阅一次once方法


在一些场景下,某些事件订阅可能只需要执行一次,后续的通知将不再响应。

实现的思路:新增 once 订阅方法,当响应了对应“发布者消息”,则主动取消订阅当前执行的回调函数。

为此新增类型,如此便于回调函数的描述信息扩展:

type SingleEvent = {

fn: () => void;

once: boolean;

};

_eventsMap的类型更改为:

private _eventsMap: Map<string, Array>;

同时抽出公共方法 addListener,供 ononce 方法共用:

private addListener( eventName: string, eventFnCallback: () => void, once = false) {

const newArr = this._eventsMap.get(eventName) || [];

newArr.push({

fn: eventFnCallback,

once,

});

this._eventsMap.set(eventName, newArr);

}

/**

* 事件订阅

* @param eventName 事件名

* @param eventFnCallback 事件发生时的回调函数

*/

public on(eventName: string, eventFnCallback: () => void) {

this.addListener(eventName, eventFnCallback);

}

/**

* 事件订阅一次

* @param eventName 事件名

* @param eventFnCallback 事件发生时的回调函数

*/

public once(eventName: string, eventFnCallback: () => void) {

this.addListener(eventName, eventFnCallback, true);

}

与此同时,我们需要考虑在触发事件时候,执行一次就需要取消订阅

/**

* 触发:主动通知并执行注册的回调函数

* @param eventName 事件名

*/

public emit(eventName: string) {

const fns = this._eventsMap.get(eventName) || [];

fns.forEach((evt, index) => {

evt.fn();

if (evt.once) fns.splice(index, 1);

});

this._eventsMap.set(eventName, fns);

}

另外取消订阅中函数中比较需要替换对象属性比较:newArr[i].fn === eventFnCallback

这样我们的事件中心支持 once 方法改造就完成了。

2.3 缓存发布消息


在框架开发下,通常会使用异步按需加载组件,如果发布者组件先发布了消息,但是异步组件还未加载完成(完成订阅注册),那么发布者的这条发布消息就不会被响应。因此,我们需要把消息做一个缓存队列,直到有订阅者订阅了,并只响应一次缓存的发布消息,该消息就会从缓存出队。

首先梳理下缓存消息的逻辑流程:

UML时序图

发布者发布消息,事件中心检测是否存在订阅者,如果没有订阅者订阅此条消息,则把该消息缓存到离线消息队列中,当有订阅者订阅时,检测是否订阅了缓存中的事件消息,如果是,则该事件的缓存消息依次出队(FCFS调度执行),触发订阅者回调函数执行一次。

新增离线消息缓存队列:

private _offlineMessageQueue: Map<string, number>;

在emit发布消息中判断对应事件是否有订阅者,没有订阅者则向离线事件消息中更新

/**

* 触发:主动通知并执行注册的回调函数

* @param eventName 事件名

*/

public emit(eventName: string) {

const fns = this._eventsMap.get(eventName) || [];

+  if (fns.length === 0) {

+    const counter = this._offlineMessageQueue.get(eventName) || 0;

+    this._offlineMessageQueue.set(eventName, counter + 1);

+    return;

+  }

fns.forEach((evt, index) => {

evt.fn();

if (evt.once) fns.splice(index, 1);

});

this._eventsMap.set(eventName, fns);

}

然后在 addListener 方法中根据离线事件消息统计的次数,重新emit发布事件消息,触发消息回调函数执行,之后删掉离线消息中的对应事件。

private addListener(

eventName: string,

eventFnCallback: () => void,

once = false

) {

const newArr = this._eventsMap.get(eventName) || [];

newArr.push({

fn: eventFnCallback,

once,

});

this._eventsMap.set(eventName, newArr);

+  const cacheMessageCounter = this._offlineMessageQueue.get(eventName);

+  if (cacheMessageCounter) {

+    for (let i = 0; i < cacheMessageCounter; i++) {

+      this.emit(eventName);

+    }

+    this._offlineMessageQueue.delete(eventName);

+  }

}

这样,一个支持离线消息的事件中心就写好了!

2.4 回调函数传参&执行环境


在上面的回调函数中,我们可以发现是一个没有返回值,没有入参的函数,这其实有些鸡肋,在函数运行的时候会指向执行的上下文,可能某些回调函数中含有this指向就无法绑定到事件中心上,因此针对回调函数需要绑定执行上下文环境。

2.4.1 支持回调函数传参

首先将TypeScript中的函数类型fn: () => void 改为 fn: Function,这样能够通过函数任意参数长度的TS校验。

其实在事件中心里回调函数是没有参数的,如有参数也是提前通过参数绑定(bind)方式传入。

另外如果真要支持回调函数传参,那么就需要在 emit() 的时候传入参数,然后再将参数传递给回调函数,这里我们暂时先不实现了。

2.4.2 执行环境绑定

在需要实现执行环境绑定这个功能前,先思考一个问题:“是应该开发者自行绑定还是应该事件中心来做?”

换句话说,开发者在 on('eventName', 回调函数) 的时候,是否应该主动绑定 this 指向?在当前设计下,初步认为无参数的回调函数自行绑定 this 比较合适。

因此,在事件中心这暂时不需要去做绑定参数的行为,如果回调函数内有需要传参、绑定执行上下文的,需要在绑定回调函数的时候自行 bind。这样,我们的事件中心也算是保证了功能的纯净性。

到这里我们自己手搓简单的发布订阅事件中心就完成了!

三、学习EventEmitter3的设计实现

======================

虽然我们按照自己的理解实现了一版,但是没有对比我们也不知道好坏,因此一起看看 EventEmitter3 这个优秀“极致性能优化”的库是怎么去处理事件订阅与发布,同时可以学习下其中的性能优化思路。

首先,EventEmitter3(后续简称:EE3)的实现思路,用Events对象作为“回调事件对象”的存储器,类比我们上述实现的“发布订阅模式”作为事件的执行逻辑,另外addListener() 函数增加了传入执行上下文环境参数,emit() 函数支持最多传入5个参数,同时EventEmitter3中还加入了监听器计数、事件名前缀。

3.1 Events存储器


避免转译,以及为了提升兼容性和性能,EventEmitter3用ES5来编写。

在JavaScript中万物是对象,函数也是对象,因此存储器的实现:

function Events() {}

3.2 事件侦听器实例


同理,我们上述使用singleEvent对象来存储每一个事件侦听器实例,EE3 中用一个EE对象存储每个事件侦听器的实例以及必要属性

/**

* 每个事件侦听器实例的表示形式

* @param {Function} fn 侦听器函数

* @param {*} context 调用侦听器的执行上下文

* @param {Boolean} [once=false] 指定侦听器是否仅支持调用一次

* @constructor

* @private

*/

function EE(fn, context, once) {

this.fn = fn;

this.context = context;

this.once = once || false;

}

3.3 添加侦听器方法


/**

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)

最后

技术是没有终点的,也是学不完的,最重要的是活着、不秃。零基础入门的时候看书还是看视频,我觉得成年人,何必做选择题呢,两个都要。喜欢看书就看书,喜欢看视频就看视频。最重要的是在自学的过程中,一定不要眼高手低,要实战,把学到的技术投入到项目当中,解决问题,之后进一步锤炼自己的技术。

技术学到手后,就要开始准备面试了,找工作的时候一定要好好准备简历,毕竟简历是找工作的敲门砖,还有就是要多做面试题,复习巩固。有需要面试题资料的朋友点击这里可以领取


减轻大家的负担。**

[外链图片转存中…(img-6gN8ufoc-1712655178355)]

[外链图片转存中…(img-2HHaChkG-1712655178356)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!

[外链图片转存中…(img-8cv34IqN-1712655178356)]

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)

最后

技术是没有终点的,也是学不完的,最重要的是活着、不秃。零基础入门的时候看书还是看视频,我觉得成年人,何必做选择题呢,两个都要。喜欢看书就看书,喜欢看视频就看视频。最重要的是在自学的过程中,一定不要眼高手低,要实战,把学到的技术投入到项目当中,解决问题,之后进一步锤炼自己的技术。

技术学到手后,就要开始准备面试了,找工作的时候一定要好好准备简历,毕竟简历是找工作的敲门砖,还有就是要多做面试题,复习巩固。有需要面试题资料的朋友点击这里可以领取

[外链图片转存中…(img-Vd1NUBZY-1712655178356)]

  • 8
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值