DEV Community

Juliano Padilha
Juliano Padilha

Posted on • Updated on

NodeJS Event Emitter

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.

Alt Text

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();
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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');
});
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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'));
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode
const eventEmitter = require('./eventEmitter');

// Registra listener
eventEmitter.on('meuEvento', () => {
    console.log('Listener!');
});

// Registra evento
eventEmitter.emit('meuEvento');
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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');
});
Enter fullscreen mode Exit fullscreen mode

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!');
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
urielsouza29 profile image
Uriel dos Santos Souza

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