场景
在日常的前端开发中,经常会遇到跨组件间的通信问题。在vue2中,提供了下面的语法来支持(在vue3中已被移除)
$on:注册监听$off:销毁监听$once:只监听一次$emit:触发事件
在vue3中你可以使用vueuse库提供的useEventBus来实现同样的效果。
同时在nodejs中,也提供了events模块,方便开发者进行异步通信。
它是发布订阅模式的一种实现,如果你对观察者模式感兴趣,大橙子推荐你可以读一读这篇文章:观察者模式,同样这也是面试题中的高频考题😄
实现
准备工作
接下来基于ES6笔者带你一步步来实现这个功能:
先创建一个EventEmitter类:
class EventEmitter {
constructor() {
// ①
this.events = new Map();
}
}
在构造函数①中,新建一个Map类型数据来管理所有的发布订阅
实现增加监听(on)功能:
class EventEmitter {
constructor() {
this.events = new Map();
}
// ①
on(type, handler) {
// ②
const handlers = this.events.get(type);
if (handlers) {
// ③
handlers.push(handler);
} else {
// ④
this.events.set(type, [handler]);
}
}
}
①:on需要传入两个参数:
type: 事件类型handler: 事件触发时,需要执行的监听函数
②:在当前存储所有发布订阅的events中获取之前是否存储过该事件类型(type)
有可能有多个地方同时订阅了一个事件,因此存储某个事件类型(type)监听函数的是一个数组。
当之前已经注册了该事件类型时,③将新的监听方法加入到数组中。
如果之前并未注册过该事件类型,④将新的事件类型及触发后监听的方法,存入events中。
实现销毁监听(off)功能
class EventEmitter {
constructor() {
this.events = new Map();
}
on(type, handler) {
const handlers = this.events.get(type);
if (handlers) {
handlers.push(handler);
} else {
this.events.set(type, [handler]);
}
}
// ①
off(type, handler) {
// ②
const handlers = this.events.get(type);
if (handlers) {
// ③
handlers.splice(handlers.indexOf(handler) >>> 0, 1);
} else {
// ④
this.events.set(type, []);
}
}
}
①:销毁监听(off)同样需要传入下面两个参数:
type: 需要销毁监听的事件类型handler: 需要销毁的监听函数
②:在当前存储所有发布订阅的events中获取之前是否存储过该事件类型(type)
在③中,如果在已有的监听列表中,找到了该监听方法,那么就把它删除掉,如果没有找到那么 -1 >>> 0将会返回一个最大的32位整数,它是数组的最大长度,因此它不会删除任何一个元素。
在④中,如果这个时间类型并不存在,那么我们将会默认设置一个时间类型,监听函数的队列为空
实现触发事件(emit)功能
class EventEmitter {
constructor() {
this.events = new Map();
}
on(type, handler) {
const handlers = this.events.get(type);
if (handlers) {
handlers.push(handler);
} else {
this.events.set(type, [handler]);
}
}
off(type, handler) {
const handlers = this.events.get(type);
if (handlers) {
handlers.splice(handlers.indexOf(handler) >>> 0, 1);
} else {
this.events.set(type, []);
}
}
// ①
emit(type, ...args) {
// ②
const handlers = this.events.get(type);
if (handlers) {
// ③
handlers.slice().map(handler => { handler(...args) });
}
}
}
①:触发事件(emit)首先需要传入的参数是具体触发的事件类型(type),以及具体需要传递给监听参数的数据,由于可能有多个参数,因此使用...args
②:在当前存储所有发布订阅的events中获取之前是否存储过该事件类型(type)
③:如果有就浅拷贝当前的监听数组,并且循环执行的同时传入...args。
实现只监听一次(once)功能
class EventEmitter {
constructor() {
this.events = new Map();
}
on(type, handler) {
const handlers = this.events.get(type);
if (handlers) {
handlers.push(handler);
} else {
this.events.set(type, [handler]);
}
}
off(type, handler) {
const handlers = this.events.get(type);
if (handlers) {
handlers.splice(handlers.indexOf(handler) >>> 0, 1);
} else {
this.events.set(type, []);
}
}
emit(type, ...args) {
const handlers = this.events.get(type);
if (handlers) {
handlers.slice().map(handler => { handler(...args) });
}
}
// ①
once(type, handler) {
// ②
const wrapper = (...args) => {
handler(...args);
this.off(type, wrapper);
}
// ③
this.on(type, wrapper);
}
}
①:实现只监听一次(once)需要传入两个参数:
type: 事件类型handler: 只执行一次的监听函数
在②中基于传入的handler,增加执行完成后,自动执行销毁监听的方法
并在③中将新的方法添加到该事件类型的监听列表中
测试
接下来测试一下刚刚写好的EventEmitter:
class EventEmitter {
constructor() {
this.events = new Map();
}
on(type, handler) {
const handlers = this.events.get(type);
if (handlers) {
handlers.push(handler);
} else {
this.events.set(type, [handler]);
}
}
off(type, handler) {
const handlers = this.events.get(type);
if (handlers) {
handlers.splice(handlers.indexOf(handler) >>> 0, 1);
} else {
this.events.set(type, []);
}
}
emit(type, ...args) {
const handlers = this.events.get(type);
if (handlers) {
handlers.slice().map(handler => { handler(...args) });
}
}
once(type, handler) {
const wrapper = (...args) => {
handler(...args);
this.off(type, wrapper);
}
this.on(type, wrapper);
}
}
const handleAlarm = (text) => {console.log(`闹钟响了!!现在是北京时间${text}`)}
const handleOnceAlarm = (text) => {console.log(`闹钟响了!!该闹钟只响一次!!现在是北京时间${text}`)}
const handleNeedDestroyAlarm = (text) => {console.log(`闹钟响了!!该闹钟响两次后会被销毁!!现在是北京时间${text}`)}
const eventEmitter = new EventEmitter();
eventEmitter.on('alarm', handleAlarm);
eventEmitter.on('alarm', handleNeedDestroyAlarm);
eventEmitter.once('alarm', handleOnceAlarm);
console.log('开始第一次响闹钟了!!');
eventEmitter.emit('alarm', new Date().toLocaleString());
console.log('开始第二次响闹钟了!!');
eventEmitter.emit('alarm', new Date().toLocaleString());
eventEmitter.off('alarm', handleNeedDestroyAlarm);
console.log('开始第三次响闹钟了!!');
eventEmitter.emit('alarm', new Date().toLocaleString());
执行后,你会得到下面的内容:
恭喜你,你已经成功实现了一个发布订阅功能!
开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 10 天,点击查看活动详情