Artigo original: How to code your own event emitter in Node.js: a step-by-step guide
Escrito por: Rajesh Pillai
Entenda o funcionamento do Node programando pequenos pacotes/módulos
Se você é novo no Node.js, há muitos tutoriais aqui no freeCodeCamp e em outros lugares. Você pode conferir meu artigo All about Core Node.JS (em inglês), por exemplo.
Sem mais delongas, vamos ao assunto em discussão: "Emissores de Eventos". Os emissores de eventos desempenham um papel muito importante no ecossistema do Node.js.
O EventEmitter (Emissor de Eventos) é um módulo que facilita a comunicação/interação entre objetos no Node. O EventEmitter está no centro da arquitetura orientada a eventos assíncrona do Node. Muitos dos módulos internos do Node são herdados do EventEmitter, incluindo frameworks proeminentes, como o Express.js.
O conceito é bastante simples: objetos emissores emitem eventos nomeados, que fazem com que ouvintes (em inglês, listeners) previamente registrados sejam chamados. Assim, um objeto emissor possui basicamente duas funcionalidades principais:
- Emitir eventos nomeados.
- Registrar e cancelar o registro de funções do ouvinte.
É meio como um padrão de desenho pub/sub ou observador (embora não exatamente).
O que criaremos neste tutorial
- classe EventEmitter
- método on/addEventListener
- método off/removeEventListener
- método once
- método emit
- método rawListeners
- método listenerCount
Os recursos básicos acima são suficientes para implementar um sistema completo usando o modelo de eventos.
Antes de entrarmos na programação, vamos dar uma olhada em como usaremos a classe EventEmitter. Observe que nosso código imitará a API exata do módulo "events" do Node.js.
Na verdade, se você substituir nosso EventEmitter pelo módulo "events" integrado do Node.js, você obterá o mesmo resultado.
Exemplo 1 - Crie uma instância de emissor de evento e registre algumas funções de retorno
const myEmitter = new EventEmitter();
function c1() {
console.log('Ocorreu um evento!');
}
function c2() {
console.log('E mais outro!');
}
myEmitter.on('eventOne', c1); // Registro para eventOne
myEmitter.on('eventOne', c2); // Registro para eventOne
Quando o evento "eventOne" é emitido, ambas as funções de retorno acima devem ser invocadas.
myEmitter.emit('eventOne');
A saída no console será a seguinte:
Ocorreu um evento!
E mais outro!
Exemplo 2 - Registrando para o evento ser disparado apenas uma vez usando "once"
myEmitter.once('eventOnce', () => console.log('eventOnce disparado apenas uma vez'));
Emitindo o evento "eventOnce":
myEmitter.emit('eventOne');
A seguinte saída deve aparecer no console:
eventOnce disparado apenas uma vez
Emitir eventos registrados com "once" novamente não terá impacto.
myEmitter.emit('eventOne');
Uma vez que o evento foi emitido apenas uma vez, a instrução acima não terá impacto.
Exemplo 3 - Registrando-se para o evento com parâmetros de função de retorno
myEmitter.on('status', (code, msg)=> console.log(`Obtive ${code} e ${msg}`));
Emitindo o evento com parâmetros:
myEmitter.emit('status', 200, 'ok');
A saída no console será a seguinte:
Obtive 200 e ok
OBSERVAÇÃO: Você pode emitir eventos várias vezes (exceto os registrados com o método "once").
Exemplo 4 - Cancelando o registro de eventos
myEmitter.off('eventOne', c1);
Agora, se você emitir o evento da seguinte forma, nada acontecerá e será um "noop":
myEmitter.emit('eventOne'); // noop
Exemplo 5 - Obtendo contagem de ouvintes
console.log(myEmitter.listenerCount('eventOne'));
OBSERVAÇÃO: Se o registro do evento foi cancelado usando o método "off" ou "removeListener", a contagem será 0.
Exemplo 6 - Obtendo ouvintes brutos
console.log(myEmitter.rawListeners('eventOne'));
Exemplo 7 - Exemplo de demonstração assíncrona
// Exemplo 2->Adaptado de um texto de Sameer Buna. Grato!
class WithTime extends EventEmitter {
execute(asyncFunc, ...args) {
this.emit('begin');
console.time('execute');
this.on('data', (data)=> console.log('got data ', data));
asyncFunc(...args, (err, data) => {
if (err) {
return this.emit('error', err);
}
this.emit('data', data);
console.timeEnd('execute');
this.emit('end');
});
}
}
Usando o emissor de evento "withTime":
const withTime = new WithTime();
withTime.on('begin', () => console.log('Prestes a executar'));
withTime.on('end', () => console.log('Execução encerrada'));
const readFile = (url, cb) => {
fetch(url)
.then((resp) => resp.json()) // Transformar os dados em json
.then(function(data) {
cb(null, data);
});
}
withTime.execute(readFile, 'https://jsonplaceholder.typicode.com/posts/1');
Verifique a saída no console. A lista de postagens será exibida junto com outros logs.
O padrão Observador para nosso emissor de evento

Diagrama visual 1 (métodos em nosso EventEmitter)

Como agora entendemos a utilização da API, vamos programar o módulo.
O código de exemplo completo para a classe EventEmitter
Estaremos preenchendo os detalhes de modo incremental nas próximas seções.
class EventEmitter {
listeners = {}; // par chave-valor
addListener(eventName, fn) {}
on(eventName, fn) {}
removeListener(eventName, fn) {}
off(eventName, fn) {}
once(eventName, fn) {}
emit(eventName, ...args) { }
listenerCount(eventName) {}
rawListeners(eventName) {}
}
Começamos criando o modelo para a classe EventEmitter junto com um objeto para armazenar os ouvintes. Os ouvintes serão armazenados como um par chave-valor. O valor pode ser uma lista (já que para o mesmo evento permitimos que vários ouvintes sejam cadastrados).
1. O método addListener()
Vamos agora implementar o método addListener. Ele recebe um nome de evento e uma função de retorno para ser executada.
addListener(event, fn) {
this.listeners[event] = this.listeners[event] || [];
this.listeners[event].push(fn);
return this;
}
Uma pequena explicação:
O evento addListener verifica se o evento já está registrado. Se estiver, retorna o array. Caso contrário, o array estará vazio.
this.listeners[event] // retornará o array de eventos ou undefined (registro inicial)
Por exemplo…
Vamos entender isso com um exemplo de uso. Vamos criar um eventEmitter e registrar um "test-event". Esta é a primeira vez que o "test-event" está sendo registrado.
const eventEmitter = new EventEmitter();
eventEmitter.addListener('test-event',
()=> { console.log ("test one") }
);
Dentro do método addListener():
this.listeners[event] => this.listeners['test-event']
=> undefined || []
=> []
O resultado será:
this.listeners['test-event'] = []; // empty array
Espero que isso torne o método "addListener" muito claro para decifrar e entender.
Uma observação: várias funções de retornos podem ser registradas para o mesmo evento.

2. O método on()
Este é apenas um apelido para o método "addListener". Usaremos o método "on" mais do que o método "addListener" por uma questão de conveniência.
on(event, fn) {
return this.addListener(event, fn);
}
3. O método removeListener(event, fn)
O método "removeListener" recebe um nome de evento e a função de retorno como parâmetros. Ele remove o ouvinte mencionado do array de eventos.
OBSERVAÇÃO: Se o evento tiver vários ouvintes, os outros ouvintes não serão afetados.
Primeiro, vamos dar uma olhada no código completo para "removeListener".
removeListener (event, fn) {
let lis = this.listeners[event];
if (!lis) return this;
for(let i = lis.length; i > 0; i--) {
if (lis[i] === fn) {
lis.splice(i,1);
break;
}
}
return this;
}
Aqui está o método "removeListener" explicado passo a passo:
- Obtém o array de ouvintes por evento.
- Se nenhum for encontrado, retornar "this" para encadeamento.
- Se encontrado, percorre todos os ouvintes. Se o ouvinte atual corresponde ao parâmetro "fn", usa o método "splice" do array para removê-lo. Depois, sai do laço.
- Retorna "this" para continuar encadeando.
4. O método off(event, fn)
Este é apenas um apelido para o método "removeListener". Estaremos usando o método "off" mais do que o método "removeListener" por uma questão de conveniência.
off(event, fn) {
return this.removeListener(event, fn);
}
5. O método once(eventName, fn)
Adiciona uma função de ouvinte única para o evento chamado "eventName". Na próxima vez que o nome do evento for acionado, esse ouvinte será removido e, em seguida, invocado.
Use para eventos do tipo configuração/inicialização.
Vamos dar uma olhada no código.
once(eventName, fn) {
this.listeners[event] = this.listeners[eventName] || [];
const onceWrapper = () => {
fn();
this.off(eventName, onceWrapper);
}
this.listeners[eventName].push(onceWrapper);
return this;
}
Aqui está o método "once" explicado passo a passo:
- Obtém o objeto de array de eventos. Se for a primeira vez, o array estará vazio.
- Cria uma função de invólucro chamada "onceWrapper", que invocará a função "fn" quando o evento for emitido e também removerá o ouvinte.
- Adiciona a função de invólucro ao array.
- Retorna "this" para encadeamento.
6. O método emit(eventName, ..args)
Chama de modo síncrono cada um dos ouvintes registrados para o evento com o nome "eventName", na ordem em que foram registrados, passando os argumentos fornecidos para cada um.
Retorna verdadeiro (true) se o evento tiver ouvintes ou falso (false), do contrário.
emit(eventName, ...args) {
let fns = this.listeners[eventName];
if (!fns) return false;
fns.forEach((f) => {
f(...args);
});
return true;
}

Aqui está o método "emit" explicado passo a passo:
- Obtém as funções registradas para o parâmetro nome do evento.
- Se não houver ouvintes, retorna
false
. - Para todos os ouvintes de função, invoca a função com os argumentos fornecidos.
- Retorna
true
quando termina.
7. O método listenerCount(eventName)
Retorna o número de ouvintes escutando o evento com o nome "eventName".
Aqui está o código-fonte:
listenerCount(eventName) {
let fns = this.listeners[eventName] || [];
return fns.length;
}
Aqui está o método "listenerCount" explicado passo a passo:
- Obtém as funções/ouvintes consideradas ou um array vazio se não houver nenhum.
- Retorna o comprimento (desse array).
8. O método rawListeners(eventName)
Retorna uma cópia do array de ouvintes para o evento com o nome "eventName", incluindo quaisquer invólucros (como os criados por .once()). Os invólucros de .once() nesta implementação não estarão disponíveis se o evento tiver sido emitido uma vez.
rawListeners(event) {
return this.listeners[event];
}
O código-fonte completo, para referência:
class EventEmitter {
listeners = {}
addListener(eventName, fn) {
this.listeners[eventName] = this.listeners[eventName] || [];
this.listeners[eventName].push(fn);
return this;
}
on(eventName, fn) {
return this.addListener(eventName, fn);
}
once(eventName, fn) {
this.listeners[eventName] = this.listeners[eventName] || [];
const onceWrapper = () => {
fn();
this.off(eventName, onceWrapper);
}
this.listeners[eventName].push(onceWrapper);
return this;
}
off(eventName, fn) {
return this.removeListener(eventName, fn);
}
removeListener (eventName, fn) {
let lis = this.listeners[eventName];
if (!lis) return this;
for(let i = lis.length; i > 0; i--) {
if (lis[i] === fn) {
lis.splice(i,1);
break;
}
}
return this;
}
emit(eventName, ...args) {
let fns = this.listeners[eventName];
if (!fns) return false;
fns.forEach((f) => {
f(...args);
});
return true;
}
listenerCount(eventName) {
let fns = this.listeners[eventName] || [];
return fns.length;
}
rawListeners(eventName) {
return this.listeners[eventName];
}
}
O código completo está disponível aqui.
Como exercício, sinta-se à vontade para implementar outras APIs de eventos da documentação (em inglês).
Se você gostou deste artigo e deseja ver mais artigos semelhantes, sinta-se à vontade para compartilhá-lo. :)
OBSERVAÇÃO: O código foi otimizado para legibilidade e não para desempenho. Talvez, como exercício, você possa otimizar o código e compartilhá-lo. Não testei completamente para casos extremos e algumas validações podem estar imprecisas, pois foi um rascunho rápido.
Este artigo faz parte de um curso em vídeo que criariei, chamado "Node.JS Master Class — Build Your Own ExpressJS-Like MVC Framework from scratch".
O título do curso, porém, ainda não é definitivo.