怎样轻松实现一个 EventEmitter?
之所以要特地讲解这部分知识,是因为虽然严格意义上来说,events
模块属于 Node.js
服务端的知识,但是由于大多数 Node.js
核心 API
构建用的是异步事件驱动架构。
在开始前请先思考几个问题:
EventEmitter
采用什么样的设计模式?EventEmitter
常用的API
是怎样实现的?
Events 基本介绍
Node.js
的events
模块对外提供了一个 EventEmitter
对象,用于对 Node.js
中的事件进行统一管理。因为 Node.js
采用了事件驱动机制,而 EventEmitter
就是 Node.js
实现事件驱动的基础。在 EventEmitter
的基础上,Node.js
中几乎所有的模块都继承了这个类,以实现异步事件驱动架构。
为了对此有一个大概的了解,先来看下 EventEmitter
的简单使用情况,代码如下。
const events = require('events');
const eventEmitter = new events.EventEmitter();
eventEmitter.on('say',(name)=>{
console.log('hello',name);
})
eventEmitter.emit('say','mark'); // hello mark
以上代码中,新定义的eventEmitter
是接收 events.EventEmitter
模块 new
之后返回的一个实例,eventEmitter
的 emit
方法,发出 say
事件,通过 eventEmitter
的 on
方法监听,从而执行相应的函数。
常用的 EventEmitter 模块的 API
除了上面的那段代码中已经使用的 on
和emit
这两个 API
,EventEmitter
还提供了其他的 API
方法,我通过一个表格简单整理了一下对应的方法和功能总结。
方法名 | 方法描述 |
---|---|
addListener(event,listener) | 为指定事件添加一个监听器到监听器数组的尾部 |
prependListener(event,listener) | 与addListener 相对,为指定事件添加一个监听器到监听器数组的头部 |
on(event,listener) | 其实就是addListener 的别名 |
removeListener(event,listener) | 移除指定事件的某个监听器,监听器必须是该事件已经标注过的监听器 |
off(event,listener) | removeListener的别名 |
removeAllListeners([event]) | 移除所有事件的所有监听器,如果指定事件,则移除指定事件的所有监听器 |
setMaxListeners(n) | 默认情况下,EventEmitters 中如果添加的监听器超过10个就会输出警告信息;setMaxListeners 函数用于提高监听器的默认限制的数量。 |
listeners(event) | 返回指定事件的监听器数组 |
emit(event,[arg1],[arg2],[…]) | 按参数的顺序执行每个监听器,如果事件有注册监听返回true,否则返回false |
once(event,listener) | 为指定事件注册一个单次监听器,即监听器最多只会触发一次,触发后立刻解除该监听器 |
除此之外,还有两个特殊的事件,不需要额外手动添加,下表所示的就是 Node.js
的 EventEmitter
模块自带的特殊事件。
事件名 | 事件描述 |
---|---|
newListener | 该事件在添加新事件监听器的时候触发 |
removeListener | 从指定监听器数组中删除一个监听器,需要注意的是,此操作会改变处于被删监听器之后的那些监听器的索引。 |
addListener 和 removeListener、on 和 off 方法对比
addListener
方法的作用是为指定事件添加一个监听器,其实和 on
方法实现的功能是一样的,on
其实就是 addListener
方法的一个别名。二者实现的作用是一样的,同时 removeListener
方法的作用是为移除某个事件的监听器,同样 off
也是 removeListener
的别名。
下面我们来看看addListener
和removeListener
的用法,请看下面一段示例代码。
const events = require('events');
const emitter = new events.EventEmitter();
const hello1 = (name)=>{
console.log("hello 1",name);
}
const hello2 = (name)=>{
console.log("hello 2",name);
}
emitter.addListener('say',hello1);
emitter.addListener('say',hello2);
emitter.emit('say','mark');
emitter.removeListener('say',hello1);
emitter.emit('say','lpb');
/*
执行结果:
hello 1 mark
hello 2 mark
hello 2 lpb
*/
结合代码和注释来理解我上面的描述,是不是对于 addListener
和 removeListener
、on
和 off
这两组方法的对比就一目了然了呢?
removeListener 和 removeAllListeners
removeListener
方法是指移除一个指定事件的某一个监听器,而 removeAllListeners
指的是移除某一个指定事件的全部监听器。
这里举一个removeAllListeners
的代码例子,请看。
const events = require('events');
const emitter = new events.EventEmitter();
const hello1 = (name)=>{
console.log("hello 1",name);
}
const hello2 = (name)=>{
console.log("hello 2",name);
}
emitter.addListener('say',hello1);
emitter.addListener('say',hello2);
emitter.removeAllListeners('say');
emitter.emit('say','John');
//removeAllListeners 移除了所有关于 say 事件的监听
//因此没有任何输出
on 和 once 方法区别
on
和 once
的区别是:on
的方法对于某一指定事件添加的监听器可以持续不断地监听相应的事件;而 once
方法添加的监听器,监听一次后,就会被消除。
const events = require("events");
const emitter = new events.EventEmitter();
const hello = (name) => {
console.log("hello", name);
};
emitter.on("say", hello);
emitter.emit("say", "John");
emitter.emit("say", "Lily");
emitter.emit("say", "Lucy");
//会输出 hello John、hello Lily、hello Lucy,之后还要加也可以继续触发
emitter.once("see", hello);
emitter.emit("see", "Tom");
//只会输出一次 hello Tom
/* 执行结果:
hello John
hello Lily
hello Lucy
hello Tom
*/
也就是说,on
方法监听的事件,可以持续不断地被触发,而 once
方法只会触发一次。
带你实现一个 EventEmitter
EventEmitter
是在Node.js
中 events
模块里封装的,那么在浏览器端实现一个这样的 EventEmitter
是否可以呢?其实自己封装一个能在浏览器中跑的EventEmitter
,并应用在你的业务代码中还是能带来不少方便的,它可以帮你实现自定义事件的订阅和发布,从而提升业务开发的便利性。
那么结合上面介绍的内容,我们一起来实现一个基础版本的EventEmitter
,包含基础的on
、 of
、emit
、once
、allof
这几个方法。
首先,请看一下 EventEmitter
的初始化代码。
function EventEmitter() {
this.__events = {}
}
EventEmitter.VERSION = '1.0.0';
从上面的代码中可以看到,先初始化了一个内部的__events
的对象,用来存放自定义事件,以及自定义事件的回调函数。
如何实现 EventEmitter
的 on
的方法,请看下面的代码
function EventEmitter() {
this.__events = {};
}
EventEmitter.VERSION = "1.0.0";
EventEmitter.prototype.on = function (eventName, listener) {
// 验证参数
if (!eventName && !listener) {
return;
}
// 回调的litener是否是函数
if (!isValidKistener(listener)) {
throw new TypeError("listener must be a function");
}
const events = this.__evnets;
const listeners = (events[eventName] = evnets[eventName] || []);
const listenerIsWrapped = typeof listener === "object";
// 不重复添加事件,判断是否有相同
if (indexOf(listeners.listener) === -1) {
listeners.push(
listenerIsWrapped
? listener
: {
listener: listener,
once: false,
}
);
}
return this;
};
//判断是否是合法的listener
const isValidListener = (listener) => {
if (typeof listener === "function") {
return true;
} else if (listener && typeof listener === "object") {
return isValidListener(listener.listener);
} else {
return false;
}
};
// 判断新增自定义事件是否存在
const indexOf = (array, item) => {
let result = -1;
item = typeof item === "object" ? item.listener : item;
for (let i = 0; i < array.length; i++) {
if (array[i].listener === item) {
result = i;
break;
}
}
return result;
};
从上面的代码中可以看出,on
方法的核心思路就是,当调用订阅一个自定义事件的时候,只要该事件通过校验合法之后,就把该自定义事件 push
到 this.__events
这个对象中存储,等需要出发的时候,则直接从通过获取 __events
中对应事件的 listener
回调函数,而后直接执行该回调方法就能实现想要的效果。
再看看 emit
方法是怎么实现触发效果的,请看下面的代码实现逻辑。
EventEmitter.prototype.emit = function (eventName,args){
// 直接通过内部对象获取对应自定义事件的回调函数
let listeners = this.__events[eventName];
if (!listeners) return ;
// 需要考虑多个listener 的情况
for (let i = 0;i<listeners.length;i++) {
let listener = listeners[i];
if (listener) {
listener.listener.apply(this,args||[]);
// 给listener中once为true的进行特殊处理
if (listener.once) {
this.off(eventName,listener.litener);
}
}
}
return this;
};
EventEmitter.prototype.off = function(eventName,listener) {
let listeners = this.__events[eventName];
if (!listener) return;
let index;
for (let i=0;i<listeners.length;i++) {
if (listeners[i] && listeners[i].listener === listener) {
index = i;
break;
}
}
// off的关键
if (typeof index !== 'undefined') {
listeners.splice(index,1,null);
}
return this;
}
从上面的代码中可以看出 emit
的处理方式,其实就是拿到对应自定义事件进行 apply
执行,在执行过程中对于一开始 once
方法绑定的自定义事件进行特殊的处理,当once
为 true
的时候,再触发 off
方法对该自定义事件进行解绑,从而实现自定义事件一次执行的效果。
最后,再看下 once
方法和 alloff
的实现。
EventEmitter.prototype.once = function (eventName,listener) {
// 直接调用on方法,once参数传入true,待执行后进行once处理
return this.on(eventName,{
listener:listener,
once:true
})
};
EventEmitter.prototype.allOff = function (eventName,listener) {
// 如果该eventName存在,则将其对应的listener的数组直接清空
if (eventName && this.__events[eventName]) {
this.__events[eventName] = [];
} else {
this.__events = {}
}
};
从上面的代码中可以看到,once
方法的本质还是调用 on
方法,只不过传入的参数区分和非一次执行的情况。当再次触发 emit
方法的时候,once
绑定的执行一次之后再进行解绑。
alloff
方法也很好理解了,其实就是对内部的__events
对象进行清空,清空之后如果再次触发自定义事件,也就无法触发回调函数了。
总结
现在,可以回过头思考一下在开篇提到的问题:EventEmitter
采用的是什么样的设计模式?其实通过上面的学习不难发现,EventEmitter
采用的正是发布-订阅模式。
另外,观察者模式和发布-订阅模式有些类似的地方,但是在细节方面还是有一些区别的,要注意别把这两个模式搞混了。发布-订阅模式其实是观察者模式的一种变形,区别在于:发布-订阅模式在观察者模式的基础上,在目标和观察者之间增加了一个调度中心。
单就浏览器端使用场景来说,其实也有运用同样的思路解决问题的工具,在 Vue
框架中不同组件之间的通讯里,有一种解决方案叫 EventBus
。和 EventEmitter
的思路类似,它的基本用途是将 EventBus
作为组件传递数据的桥梁,所有组件共用相同的事件中心,可以向该中心注册发送事件或接收事件,所有组件都可以收到通知,使用起来非常便利,其核心其实就是发布-订阅模式的落地实现。