深入探讨:Node.js中的EventEmitter与浏览器
在Node.js中,EventEmitter是一个非常重要的模块,它是Node.js实现事件驱动的基础。
在浏览器中,EventTarget是一个非常重要的接口,它是浏览器实现事件驱动的基础。
本文将深入探讨Node.js中的EventEmitter与浏览器中的EventTarget的实现细节,以及它们之间的异同。
EventEmitter
在Node.js的events模块中,EventEmitter是一个非常重要的类,很多模块都继承了它,比如Stream、HTTP、Net等。
EventEmitter的使用非常简单,它提供了on、once、emit、removeListener等方法,用于注册事件监听器、触发事件、移除事件监听器等。
const EventEmitter = require('events').EventEmitter;
// 创建一个EventEmitter实例
const emitter = new EventEmitter();
// 注册一个事件监听器
emitter.on('event', () => {
console.log('event emitted');
});
// 触发事件
emitter.emit('event');
非常熟悉的使用方式,这种方式在我们使用js的一些操作非常常见,例如:
const button = document.querySelector('button');
button.addEventListener('click', () => {
console.log('button clicked');
});
// 使用 jQuery 就更加熟悉了
$('button').on('click', () => {
console.log('button clicked');
});
而使用addEventListener注册事件监听器在EventEmitter也是有的,不过它叫做addListener,on和addListener是等价的。
const EventEmitter = require('events').EventEmitter;
const emitter = new EventEmitter();
// 注册一个事件监听器
emitter.addListener('event', () => {
console.log('event emitted');
});
// 触发事件
emitter.emit('event');
只不过addListener已经被标记为deprecated了,所以我们一般使用on。
EventEmitter 的属性和方法
上面已经提到了EventEmitter提供了on、once、emit、removeListener等方法,除此之外,它还提供了一些其他的方法。
这些方法我们简单的认识一下常用的就可以了,因为它们的作用都是为了让我们更方便的使用EventEmitter。
addListener:注册事件监听器,等价于on。on:注册事件监听器,接受两个参数,第一个参数是事件名称,第二个参数是事件监听器。once:注册事件监听器,只会触发一次,触发后会自动移除。removeListener:移除事件监听器,接受两个参数,第一个参数是事件名称,第二个参数是事件监听器。removeAllListeners:移除所有事件监听器,如果指定了事件名称,则只会移除指定事件的所有事件监听器。off:移除事件监听器,等价于removeListener。
其他的方法我们可以在官方文档中查看。
EventEmitter 的一些用法
EventEmitter本身在nodejs中就被广泛的使用,并且nodejs把这个功能放开了,我们则可以使用它来实现一些自己的功能,现在我们就来看一些例子。
我们可以使用EventEmitter来实现一个简单的事件总线,这个事件总线可以用来在不同的模块之间传递事件。
// event-bus.js
const EventEmitter = require('events').EventEmitter;
const eventBus = new EventEmitter();
module.exports = eventBus;
// module-a.js
const eventBus = require('./event-bus');
eventBus.on('event', () => {
console.log('event emitted');
});
// module-b.js
const eventBus = require('./event-bus');
eventBus.emit('event');
当然这是最基础的用法,正常情况下都是这样使用的,代码很简单,但是却可以实现很多功能,例如在nodejs中,Stream模块就是使用EventEmitter来实现的。
上面截图就是Stream模块的源码,我们可以看到Stream模块继承了EventEmitter,并且在Stream模块中使用了EventEmitter的一些方法。
EventTarget
在nodejs中也有一个EventTarget,他的API和浏览器中的EventTarget是一样的;
但是在nodejs中的EventTarget和浏览器中的EventTarget是不一样的,它们的实现方式也不一样。
最大的差别是nodejs中的事件不会冒泡,而浏览器中的事件会冒泡,这里要说的就是浏览器中的EventTarget。
EventTarget 的属性和方法
EventTarget提供了addEventListener、removeEventListener、dispatchEvent等方法,我们先来看一下这些方法的作用。
addEventListener:注册事件监听器,接受三个参数,第一个参数是事件名称,第二个参数是事件监听器,第三个参数是一个对象,用于指定事件监听器的一些配置。removeEventListener:移除事件监听器,接受三个参数,第一个参数是事件名称,第二个参数是事件监听器,第三个参数是一个对象,用于指定事件监听器的一些配置。dispatchEvent:触发事件,接受一个参数,这个参数是一个事件对象。
就这三个方法,没有其他的了,详情可以查看官方文档。
EventTarget 的一些用法
EventTarget在浏览器中的使用非常广泛,其中的地位就像EventEmitter在nodejs中的地位一样,我们可以使用EventTarget来实现一些功能。
但是通常我们使用也和nodejs中的EventEmitter一样,道理是这个道理,但是使用场景还是不同。
通常也是继承EventTarget来实现一些功能,上面给的是nodejs中最简单的例子,我们来看一个稍微高级点的用法:
例如我们可以保证同一时间只有一个请求发送到服务器,这样就可以避免发送重复的请求。
const eventTarget = new EventTarget();
let isSending = false;
const fetchApi = (callback) => {
const _callback = (event) => {
callback(event);
eventTarget.removeEventListener('send', _callback);
};
eventTarget.addEventListener('send', _callback);
if (isSending) {
return;
}
isSending = true;
const event = new Event('send');
fetch('/api').then((res) => {
// do something
event.data = res;
}).catch((err) => {
event.error = err;
}).finally(() => {
isSending = false;
eventTarget.dispatchEvent(event);
});
}
fetchApi((res) => {
console.log(res);
});
fetchApi((res) => {
console.log(res);
});
fetchApi((res) => {
console.log(res);
});
可以看到有三次输出,但是报错只有一个404,这说明只有一次请求发送到了服务器。
区别
EventEmitter和EventTarget都是用来实现事件的,但是它们之间还是有一些区别的。
首先就是上面说过的例子,EventTarget会有事件冒泡,而EventEmitter没有事件冒泡。
因为在nodejs中是不存在层级关系的,所有的模块都是平级的,所以EventEmitter也就没有事件冒泡。
而在浏览器中,DOM元素天然就有层级关系,事件冒泡也就是自然而然的事情了,如果没有事件冒泡,那么事件的传递就会变得非常麻烦。
其次就是EventEmitter和EventTarget的实现方式不同;
EventEmitter是一个发布订阅模式的实现,而EventTarget是一个观察者模式的实现。
这两种模式的区别在于,发布订阅模式中,发布者和订阅者是没有关系的,发布者只负责发布事件,订阅者只负责订阅事件,发布者和订阅者之间没有任何关系。
而观察者模式中,观察者和被观察者是有关系的,观察者会观察被观察者的变化,当被观察者发生变化时,观察者会收到通知。
这也是为什么EventEmitter中事件触发是可以直接通过字符串,而EventTarget中事件触发需要通过事件对象的原因。
同时也是因为这个原因他们的API也有所不同,EventEmitter中会有once这种只执行一次就移除的方法,而EventTarget中没有这种方法。
在EventTarget必须得自己手动处理只执行一次的逻辑,这也是为什么上面的例子中会有这样的代码:
eventTarget.addEventListener('send', (event) => {
callback(event);
eventTarget.removeEventListener('send', callback);
});
其实用EventEmitter的话,就不需要这样的代码了,因为EventEmitter中有once方法,可以直接这样写:
eventEmitter.once('send', callback);
在nodejs的events模块中,其实也有EventTarget的实现,但是它并不是继承EventEmitter的,而是改写了EventEmitter的实现,使其符合EventTarget的规范。
他们的区别同样也是没有事件冒泡,同时运行环境不同,服务的对象也不同。
总结
EventEmitter和EventTarget都是用来实现事件的,但是它们之间还是有一些区别的。
最大的区别就是一个是node环境,一个是浏览器环境,为了服务不同的环境,它们的实现方式也不同。
EventEmitter是一个发布订阅模式的实现,而EventTarget是一个观察者模式的实现。
EventEmitter中事件触发是可以直接通过字符串,而EventTarget中事件触发需要通过事件对象。
