Comprender los componentes internos del nodo codificando pequeños paquetes / módulos

Si es nuevo en Node.js, hay muchos tutoriales aquí en Medium y en otros lugares. Puede consultar mi artículo Todo sobre el núcleo Node.JS, por ejemplo.
Pero sin más preámbulos, vayamos al tema en discusión: “Emisores de eventos”. Los emisores de eventos juegan un papel muy importante en el ecosistema de Node.js.
EventEmitter es un módulo que facilita la comunicación / interacción entre objetos en Node. EventEmitter es el núcleo de la arquitectura controlada por eventos asincrónica de Node. Muchos de los módulos integrados de Node heredan de EventEmitter, incluidos marcos destacados como Express.js.
El concepto es bastante simple: los objetos emisores emiten eventos con nombre que hacen que se llame a oyentes previamente registrados. Entonces, un objeto emisor básicamente tiene dos características principales:
- Emitir eventos de nombre.
- Registrar y anular el registro de funciones de escucha.
Es como un patrón de diseño de pub / sub u observador (aunque no exactamente).
Lo que construiremos en este tutorial
- Clase EventEmitter
- on / addEventListener método
- método off / removeEventListener
- método una vez
- método de emisión
- método rawListeners
- método listenerCount
Las características básicas anteriores son suficientes para implementar un sistema completo utilizando el modelo de eventos.
Antes de entrar en la codificación, echemos un vistazo a cómo usaremos la clase EventEmitter. Tenga en cuenta que nuestro código imitará la API exacta del módulo de 'eventos' de Node.js.
De hecho, si reemplaza nuestro EventEmitter con el módulo 'eventos' incorporado de Node.js, obtendrá el mismo resultado.
Ejemplo 1: crear una instancia de emisor de eventos y registrar un par de devoluciones de llamada
const myEmitter = new EventEmitter(); function c1() { console.log('an event occurred!'); } function c2() { console.log('yet another event occurred!'); } myEmitter.on('eventOne', c1); // Register for eventOne myEmitter.on('eventOne', c2); // Register for eventOne
Cuando se emite el evento 'eventOne', se deben invocar las dos devoluciones de llamada anteriores.
myEmitter.emit('eventOne');
La salida en la consola será la siguiente:
an event occurred! yet another event occurred!
Ejemplo 2— Registrarse para que el evento se active solo una vez usando una vez.
myEmitter.once('eventOnce', () => console.log('eventOnce once fired'));
Emitiendo el evento 'eventOnce':
myEmitter.emit('eventOne');
La siguiente salida debería aparecer en la consola:
eventOnce once fired
Emitir eventos registrados con una vez más no tendrá ningún impacto.
myEmitter.emit('eventOne');
Dado que el evento solo se emitió una vez, la declaración anterior no tendrá ningún impacto.
Ejemplo 3: registro para el evento con parámetros de devolución de llamada
myEmitter.on('status', (code, msg)=> console.log(`Got ${code} and ${msg}`));
Emitiendo el evento con parámetros:
myEmitter.emit('status', 200, 'ok');
La salida en la consola será la siguiente:
Got 200 and ok
NOTA: Puede emitir eventos varias veces (excepto los registrados con el método once).
Ejemplo 4: anular el registro de eventos
myEmitter.off('eventOne', c1);
Ahora, si emite el evento de la siguiente manera, no pasará nada y será un error:
myEmitter.emit('eventOne'); // noop
Ejemplo 5: obtener el recuento de oyentes
console.log(myEmitter.listenerCount('eventOne'));
NOTA: Si se anuló el registro del evento con el método off o removeListener, el recuento será 0.
Ejemplo 6: obtención de oyentes sin procesar
console.log(myEmitter.rawListeners('eventOne'));
Ejemplo 7: demostración de ejemplo asíncrono
// Example 2->Adapted and thanks to Sameer Buna 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 el emisor de eventos withTime:
const withTime = new WithTime(); withTime.on('begin', () => console.log('About to execute')); withTime.on('end', () => console.log('Done with execute')); const readFile = (url, cb) => { fetch(url) .then((resp) => resp.json()) // Transform the data into json .then(function(data) { cb(null, data); }); } withTime.execute(readFile, '//jsonplaceholder.typicode.com/posts/1');
Verifique la salida en la consola. La lista de publicaciones se mostrará junto con otros registros.
El patrón de observador para nuestro emisor de eventos

Diagrama visual 1 (métodos en nuestro EventEmitter)

Como ahora entendemos el uso de la API, empecemos a codificar el módulo.
El código estándar completo para la clase EventEmitter
Completaremos los detalles de forma incremental en las próximas dos secciones.
class EventEmitter { listeners = {}; // key-value pair addListener(eventName, fn) {} on(eventName, fn) {} removeListener(eventName, fn) {} off(eventName, fn) {} once(eventName, fn) {} emit(eventName, ...args) { } listenerCount(eventName) {} rawListeners(eventName) {} }
We begin by creating the template for the EventEmitter class along with a hash to store the listeners. The listeners will be stored as a key-value pair. The value could be an array (since for the same event we allow multiple listeners to be registered).
1. The addListener() method
Let us now implement the addListener method. It takes in an event name and a callback function to be executed.
addListener(event, fn) []; this.listeners[event].push(fn); return this;
A little explanation:
The addListener event checks if the event is already registered. If yes, returns the array, otherwise empty array.
this.listeners[event] // will return array of events or undefined (first time registration)
For example…
Let’s understand this with a usage example. Let’s create a new eventEmitter and register a ‘test-event’. This is the first time the ‘test-event’ is being registered.
const eventEmitter = new EventEmitter(); eventEmitter.addListener('test-event', ()=> { console.log ("test one") } );
Inside addListener () method:
this.listeners[event] => this.listeners['test-event'] => undefined || [] => []
The result will be:
this.listeners['test-event'] = []; // empty array
and then the ‘fn’ will be pushed to this array as shown below:
this.listeners['test-event'].push(fn);
I hope this makes the ‘addListener’ method very clear to decipher and understand.
A note: Multiple callbacks can be registered against that same event.

2. The on method
This is just an alias to the ‘addListener’ method. We will be using the ‘on’ method more than the ‘addListener’ method for the sake of convenience.
on(event, fn) { return this.addListener(event, fn); }
3. The removeListener(event, fn) method
The removeListener method takes an eventName and the callback as the parameters. It removes said listener from the event array.
NOTE: If the event has multiple listeners then other listeners will not be impacted.
First, let’s take a look at the full code for 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; }
Here’s the removeListener method explained step-by-step:
- Grab the array of listeners by ‘event’
- If none found return ‘this’ for chaining.
- If found, loop through all listeners. If the current listener matches with the ‘fn’ parameter use the splice method of the array to remove it. Break from the loop.
- Return ‘this’ to continue chaining.
4. The off(event, fn) method
This is just an alias to the ‘removeListener’ method. We will be using the ‘on’ method more than the ‘addListener’ method for sake of convenience.
off(event, fn) { return this.removeListener(event, fn); }
5. The once(eventName, fn) method
Adds a one-timelistener
function for the event named eventName
. The next time eventName
is triggered, this listener is removed and then invoked.
Use for setup/init kind of events.
Let’s take a peek at the code.
once(eventName, fn) { this.listeners[event] = this.listeners[eventName] || []; const onceWrapper = () => { fn(); this.off(eventName, onceWrapper); } this.listeners[eventName].push(onceWrapper); return this; }
Here’s the once method explained step-by-step:
- Get the event array object. Empty array if the first time.
- Create a wrapper function called onceWrapper which will invoke the fn when the event is emitted and also removes the listener.
- Add the wrapped function to the array.
- Return ‘this’ for chaining.
6. The emit (eventName, ..args) method
Synchronously calls each of the listeners registered for the event named eventName
, in the order they were registered, passing the supplied arguments to each.
Returns true
if the event had listeners, false
otherwise.
emit(eventName, ...args) { let fns = this.listeners[eventName]; if (!fns) return false; fns.forEach((f) => { f(...args); }); return true; }

Here’s the emit method explained step-by-step:
- Get the functions for said eventName parameter
- If no listeners, return false
- For all function listeners, invoke the function with the arguments
- Return true when done
7. The listenerCount (eventName) method
Returns the number of listeners listening to the event named eventName
.
Here’s the source code:
listenerCount(eventName)
Here’s the listenerCount method explained step-by-step:
- Get the functions/listeners under consideration or an empty array if none.
- Return the length.
8. The rawListeners(eventName) method
Returns a copy of the array of listeners for the event named eventName
, including any wrappers (such as those created by .once()
). The once wrappers in this implementation will not be available if the event has been emitted once.
rawListeners(event) { return this.listeners[event]; }
The full source code for reference:
class EventEmitter { listeners = {} addListener(eventName, fn) 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) rawListeners(eventName) { return this.listeners[eventName]; } }
The complete code is available here:
//jsbin.com/gibofab/edit?js,console,output
As an exercise feel free to implement other events’ APIs from the documentation //nodejs.org/api/events.html.
If you liked this article and want to see more of similar articles, feel free to give a couple of claps :)
NOTE: The code is optimized for readability and not for performance. Maybe as an exercise, you can optimize the code and share it in the comment section. Haven’t tested fully for edge cases and some validations may be off as this was a quick writeup.
This article is part of the upcoming video course “Node.JS Master Class — Build Your Own ExpressJS-Like MVC Framework from scratch”.
The title of the course is not yet finalized.