异步编程中的发布/订阅模式(Node 示例)
背景
在开发复杂页面时经常会遇到多个模块之间的通信,这些模块是异步加载,彼此之间有通信诉求
真实场景:在协同文档中打开文档后,编辑器模块需要通知目录模块打开目录并定位到对应的文档。编辑器和目录的加载顺序不一致,在编辑器加载完成触发事件时,目录可能还没完成初始化,所以监听不到对应的事件
需求:异步模块之间的事件不受严格的加载时间限制,不丢失事件
发布订阅模式
首先看一下发布订阅模式是什么,直接上代码
// event.js 文件
class EventManager {
constructor() {
this.events = {}; // 存储所有的事件和对应的处理函数
}
on(eventName, handler) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(handler);
}
emit(eventName, data) {
if (this.events[eventName]) {
this.events[eventName].forEach((handler) => handler(data));
}
}
}
const em = new EventManager();
// 订阅事件
em.on("event", function (data) {
console.log("event 发生了, 数据为:", data);
});
// 发布事件
setTimeout(() => {
em.emit("event", "hello");
});
module.export = new EventManager()
这样就是一个简单的发布订阅模式
异步编程中的发布订阅
回到正题,如果咱们是在异步模块里使用呢,首先模拟一个异步加载监听的场景:
main 文件用来做加载逻辑控制
// main.js
require("./a");
require("./b");
a 文件中模拟一个加载耗时 1s 的异步模块,加载完成后监听事件
// a.js
const eventManager = require("./event");
setTimeout(() => {
console.log("模块 a 已加载,注册事件处理器");
eventManager.on("event", (data) => {
console.log("模块一处理事件event: " + data);
});
}, 1000);
b 文件直接同步触发事件
const eventManager = require("./event");
console.log("模块 b 已加载, 触发事件");
eventManager.emit("event", "Hello, World!");
当前的效果:
可以发现模块 a 加载的时候并没有触发 1s 前已经触发了的事件
改造其实很简单,我们只需要在触发事件的时候,把没有监听的事件给存下来,然后监听事件的时候检查当前是否有已经发生了但是没有被消费的事件,有则执行即可。修改后的代码:
// event.js
class EventManager {
constructor() {
this.events = {}; // 存储所有的事件和对应的处理函数
this.eventHappened = {}; // 存储已经发生的事件
}
on(eventName, handler) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(handler);
// 处理已经发送了的事件
if (this.eventHappened[eventName]) {
handler(this.eventHappened[eventName]);
delete this.eventHappened[eventName];
}
}
emit(eventName, data) {
if (this.events[eventName]) {
this.events[eventName].forEach((handler) => handler(data));
} else {
// 如果没有处理函数,存储发生的事件
this.eventHappened[eventName] = data;
}
}
}
module.exports = new EventManager(3000);
来看看效果
看着是不是符合预期了?其实目前还是有问题,如果再来一个模块 c 也监听 event 事件,那么模块 c 将触发不了 event 事件
// c.js
const eventManager = require("./event");
setTimeout(() => {
console.log("模块 c 已加载,注册事件处理器");
eventManager.on("event", (data) => {
console.log("模块 c 处理事件event: " + data);
});
}, 2000);
聪明的同学可能已经知道怎么修改了。我们在触发事件时,如果没有处理函数,定时删除对应的事件,这样能够同时解决同一个事件多个订阅者和事件有效期的问题。
代码如下:
class EventManager {
constructor(expiration) {
this.events = {}; // 存储所有的事件和对应的处理函数
this.eventHappened = {}; // 存储已经发生的事件
this.expiration = expiration; // 事件有效期,即一个事件被触发之后的一段时间内如果有监听事件则执行
}
on(eventName, handler) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(handler);
// 处理已经发送了的事件
if (this.eventHappened[eventName]) {
handler(this.eventHappened[eventName]);
}
}
emit(eventName, data) {
if (this.events[eventName]) {
this.events[eventName].forEach((handler) => handler(data));
} else {
// 如果没有处理函数,存储发生的事件
this.eventHappened[eventName] = data;
// 事件有效期到了之后就删除该事件
setTimeout(() => {
delete this.eventHappened[eventName];
}, this.expiration);
}
}
}
module.exports = new EventManager(3000);
再来一个模块 d,4 秒后(event 事件已过期)再监听事件
const eventManager = require("./event");
setTimeout(() => {
console.log("模块 d 已加载,注册事件处理器,此时事件有效期已过");
eventManager.on("event", (data) => {
console.log("模块 d 处理事件event: " + data);
});
}, 4000);
结果如下:
总结
至此,咱们开始的目标就完成啦。当前发布/订阅模式还有 once 等方法,大家可以当做熟悉该模式的扩展自行实现哦~
如果觉得本篇文章对您有帮助的话,请给一个👍🏻哦
转载自:https://juejin.cn/post/7259650149076025404