likes
comments
collection
share

深入探讨:Node.js中的EventEmitter与浏览器中的EventTarget

作者站长头像
站长
· 阅读数 54

Node.js中,EventEmitter是一个非常重要的模块,它是Node.js实现事件驱动的基础。

在浏览器中,EventTarget是一个非常重要的接口,它是浏览器实现事件驱动的基础。

本文将深入探讨Node.js中的EventEmitter与浏览器中的EventTarget的实现细节,以及它们之间的异同。

EventEmitter

Node.jsevents模块中,EventEmitter是一个非常重要的类,很多模块都继承了它,比如StreamHTTPNet等。

EventEmitter的使用非常简单,它提供了ononceemitremoveListener等方法,用于注册事件监听器、触发事件、移除事件监听器等。

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也是有的,不过它叫做addListeneronaddListener是等价的。

const EventEmitter = require('events').EventEmitter;

const emitter = new EventEmitter();

// 注册一个事件监听器
emitter.addListener('event', () => {
  console.log('event emitted');
});

// 触发事件
emitter.emit('event');

只不过addListener已经被标记为deprecated了,所以我们一般使用on

EventEmitter 的属性和方法

上面已经提到了EventEmitter提供了ononceemitremoveListener等方法,除此之外,它还提供了一些其他的方法。

这些方法我们简单的认识一下常用的就可以了,因为它们的作用都是为了让我们更方便的使用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来实现的。

深入探讨:Node.js中的EventEmitter与浏览器中的EventTarget

上面截图就是Stream模块的源码,我们可以看到Stream模块继承了EventEmitter,并且在Stream模块中使用了EventEmitter的一些方法。

EventTarget

nodejs中也有一个EventTarget,他的API和浏览器中的EventTarget是一样的;

但是在nodejs中的EventTarget和浏览器中的EventTarget是不一样的,它们的实现方式也不一样。

最大的差别是nodejs中的事件不会冒泡,而浏览器中的事件会冒泡,这里要说的就是浏览器中的EventTarget

EventTarget 的属性和方法

EventTarget提供了addEventListenerremoveEventListenerdispatchEvent等方法,我们先来看一下这些方法的作用。

  • addEventListener:注册事件监听器,接受三个参数,第一个参数是事件名称,第二个参数是事件监听器,第三个参数是一个对象,用于指定事件监听器的一些配置。
  • removeEventListener:移除事件监听器,接受三个参数,第一个参数是事件名称,第二个参数是事件监听器,第三个参数是一个对象,用于指定事件监听器的一些配置。
  • dispatchEvent:触发事件,接受一个参数,这个参数是一个事件对象。

就这三个方法,没有其他的了,详情可以查看官方文档

EventTarget 的一些用法

EventTarget在浏览器中的使用非常广泛,其中的地位就像EventEmitternodejs中的地位一样,我们可以使用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);
});

深入探讨:Node.js中的EventEmitter与浏览器中的EventTarget

可以看到有三次输出,但是报错只有一个404,这说明只有一次请求发送到了服务器。

区别

EventEmitterEventTarget都是用来实现事件的,但是它们之间还是有一些区别的。

首先就是上面说过的例子,EventTarget会有事件冒泡,而EventEmitter没有事件冒泡。

因为在nodejs中是不存在层级关系的,所有的模块都是平级的,所以EventEmitter也就没有事件冒泡。

而在浏览器中,DOM元素天然就有层级关系,事件冒泡也就是自然而然的事情了,如果没有事件冒泡,那么事件的传递就会变得非常麻烦。

其次就是EventEmitterEventTarget的实现方式不同;

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);

nodejsevents模块中,其实也有EventTarget的实现,但是它并不是继承EventEmitter的,而是改写了EventEmitter的实现,使其符合EventTarget的规范。

他们的区别同样也是没有事件冒泡,同时运行环境不同,服务的对象也不同。

总结

EventEmitterEventTarget都是用来实现事件的,但是它们之间还是有一些区别的。

最大的区别就是一个是node环境,一个是浏览器环境,为了服务不同的环境,它们的实现方式也不同。

EventEmitter是一个发布订阅模式的实现,而EventTarget是一个观察者模式的实现。

EventEmitter中事件触发是可以直接通过字符串,而EventTarget中事件触发需要通过事件对象。

转载自:https://juejin.cn/post/7249286832160358457
评论
请登录