O que é o Event Emitter?
Event Emitter é um módulo do NodeJS que nos ajuda a criar um padrão de publisher-subscriber.
Com um Event Emitter podemos simplesmente criar um evento de qualquer parte de uma aplicação, escutar este evento e tomar alguma ação baseado nele.
Criando um Event Emitter
Para criar um event emitter é preciso instanciar o EventEmitter
do módulo events
no NodeJS.
const { EventEmitter } = require('events');
const eventEmitter = new EventEmitter();
Esse é o básico para criar um event emitter. Mas o que podemos fazer com ele? 🤔
Publicando eventos e ouvindo-os
A classe do EventEmitter têm uma série de métodos e podemos usá-los para publicar e ouvir eventos. Por hora, focaremos em dois deles:
emit(eventName...)
on(eventName...)
Para publicar um evento nós utilizamos o método emit()
. Já para ouvir, utilizamos o método on()
. E a forma como fazemos isso utilizando EventEmitters é por meio de "nomes".
Continuando o código acima onde instanciamos a classe EventEmitter, agora podemos registrar um evento para posteriormente escutá-lo.
const { EventEmitter } = require('events');
const eventEmitter = new EventEmitter();
// Escuta o evento
eventEmitter.on('meuEvento', () => {
console.log('Dados recebidos!');
});
// Emite o evento
eventEmitter.emit('meuEvento');
Output ao rodar o código: > Dados recebidos
No exemplo acima, na última linha de código, criamos um evento com o nome de "meuEvento". E temos um listener (algo capaz de escutar o evento, que nesse caso é o método on
) algumas linhas de código acima da publicação do evento.
No momento que publicamos um evento, é preciso que já exista um listener para nosso evento que foi publicado. Então, se por exemplo, tivermos o seguinte código...
const { EventEmitter } = require('events');
const eventEmitter = new EventEmitter();
// Escuta o evento
eventEmitter.on('meuEvento', () => {
console.log('Listener 1');
});
// Emite o evento
eventEmitter.emit('meuEvento');
// Escuta evento
eventEmitter.on('meuEvento', () => {
console.log('Listener 2');
});
Output ao rodar o código: > Listener 1
Temos apenas a primeira escuta sendo executa pois ela foi registrada antes do evento ter sido emitido. Diferente do segundo listener, que apenas ocorre após o evento já ter sido publicado e com isso não é executado.
Uma instância do EventEmitter deve ser Singleton para um único nome de evento
Em outras palavras, tanto o método on()
quanto o emit()
devem ser chamados na mesma instância do EventEmitter. Se registramos o evento em uma instância e tentarmos escutá-los em outra instância, não irá funcionar.
const { EventEmitter } = require('events');
// Primeira instância
const eventEmitter1 = new EventEmitter();
eventEmitter1.on('meuEvento', () => {
console.log('Listener');
});
// Segunda instância
const eventEmitter2 = new EventEmitter();
eventEmitter2.emit('meuEvento');
Se tentarmos rodar esse código, não teremos nenhum retorno, pois existem duas instâncias separadas sendo utilizadas: uma para registrar o evento e a outra para escutar ele.
Manter uma única instância do EventEmitter para toda a aplicação
Se não podemos ter mais de uma instância do EventEmitter para um mesmo nome de evento, como podemos fazer para uma aplicação completa trabalhar com apenas uma única criação?
Para isso, existe uma estratégia para criar e manter uma cópia única de uma instância caso estejamos utilizando o express
, por exemplo.
Quando for criar o EventEmitter, podemos guardar sua instância como uma configuração a nível de aplicação usando o app.set(<key>, <value>)
.
const { EventEmitter } = require('events');
const express = require('express');
const eventEmitter = new EventEmitter();
const app = express();
app.set('eventEmitter', eventEmitter);
// Acessamos a partir de qualquer módulo da aplicação
console.log(app.get('eventEmitter'));
Outra alternativa é criar um módulo responsável por instanciar a classe do EventEmitter e exportar essa instância para ser utilizada em outras partes da aplicação.
const { EventEmitter } = require('events');
const eventEmitter = new EventEmitter();
...
...
module.exports = eventEmitter;
const eventEmitter = require('./eventEmitter');
// Registra listener
eventEmitter.on('meuEvento', () => {
console.log('Listener!');
});
// Registra evento
eventEmitter.emit('meuEvento');
Síncrono ou Assíncrono?
O NodeJS é assíncrono, mas como nenhum I/O está envolvido na emissão de um evento, a entrega do evento é tratada de forma síncrona na iteração atual do event loop do NodeJS.
Podemos comprovar isso executando o seguinte código:
const { EventEmitter } = require('events');
const eventEmitter = new EventEmitter();
eventEmitter.on('meuEvento', (data) => {
console.log(data);
});
console.log('Log 1');
eventEmitter.emit('meuEvento', 'Log 2');
console.log('Log 3');
Temos como output:
> Log 1
> Log 2
> Log 3
Isso confirma a premissa de que existe uma ordem de execução, sendo que os listeners são executados na ordem em que são criados para um evento. Podemos levar em consideração esse outro exemplo:
const { EventEmitter } = require('events');
const eventEmitter = new EventEmitter();
eventEmitter.on('meuEvento', (data) => {
console.log(data, ' - Primeiro');
});
console.log('Log 1');
eventEmitter.on('meuEvento', (data) => {
console.log(data, ' - Segundo');
});
eventEmitter.emit('meuEvento', 'Evento emitido');
console.log('Log 2');
Onde temos o output:
> Log 1
> Evento emitido - Primeiro
> Evento emitido - Segundo
> Log 2
Como e onde o NodeJS usa internamente o Event Emitter
O NodeJS utiliza internamente eventos em diversos pontos do seu ambiente. Um dos casos de uso são as Streams. Streams são construídos em cima do módulo EventEmitter e possuem eventos pré-definidos, como: open
, end
e data
.
const { createReadStream } = require('fs');
let data = '';
const readerStream = createReadStream('./file.txt');
readerStream.on('open', () => {
console.log('Abrindo evento...');
});
readerStream.on('data', chuck => {
data += chuck;
});
readerStream.on('end', () => {
console.log(data);
console.log('Finalizando evento');
});
Os próprios streams estendem naturalmente e internamente os listeners do event emitter, com isso, não precisamos importar e declarar explicitamente uma nova instância da classe EventEmitter.
Outro exemplo do uso de eventos dentro do Node é o objeto global process
. O process
expõe métodos e variáveis que podem emitir eventos e responder por eles.
process.on("exit", () => console.log("Saída!"));
process.on('uncaughtException', () => {
console.log('Exception lançada');
process.exit();
});
throw new Error('Erro!');
Conclusão
Existem diversos outros métodos da classe EventEmitter que são úteis. Suas aplicações dependem de necessidades específicas, como o once()
que escuta apenas a primeira ocorrência de um evento emitido e descarta todas as outras. Ou o eventNames()
que retorna o nome de todos os eventos ativos. A lista de funções é bem completa. 🙂
Se tiver curiosidade em tentar entender como seria uma implementação do zero da classe Event Emitter do NodeJS, abaixo deixo um exemplo desenvolvido por mim.
https://stackblitz.com/edit/jp-event-emitter?embed=1&file=index.js
Top comments (1)
E a questão de retirar os ouvintes?
Como funciona?
Li em N lugares tirar os ouvintes. Para não ficarem na memoria, ou algo assim
Mas não tem bom código exemplificando!
Você sabe algo sobre isso?
Poderia escrever sobre?
Com algum exemplo e código
Abraços