2.2EventEmitter y programación por eventos
Lo esencial en 30 segundos
- Emitir y luego escuchar: .on(registro) declara la reacción, .emit(disparo) la llama — ambos lados no se conocen.
- En todas partes bajo el capó: servidor HTTP, streams, sockets son EventEmitters — saber escuchar es saber usar Node.
- error no perdona: un evento error emitido sin escucha hace caer el programa — escuchar siempre los errores.
Minicaso hilado — Salim señala el progreso de la importación
Salim hace evolucionar su inventario (2.1): la importación de informes lleva tiempo, y el equipo quiere seguir el progreso sin llamar a su script en bucle. Crea un emisor que publica un evento por cada archivo tratado y un evento de fin — el indicador de progreso y el registro escuchan de forma independiente, sin que la importación los conozca. En 2.3, serán los propios streams los que emitirán los eventos data/end, y en 2.4 el servidor HTTP emitirá una petición con cada visita.
Continuación del hilo 2.1: el inventario de Salim se desacopla con eventos — y continúa en 2.3 y 2.4.
1. Emitir eventos, escuchar y reaccionar
Node es, en esencia, por eventos (recuerdo del nivel 0: el bucle de eventos). La programación por eventos se basa en una idea simple: en lugar de que el código llame directamente a otras funciones, unos objetos emiten eventos («ha pasado algo») y otras partes del código los escuchan y reaccionan. Es un mecanismo de desacoplamiento: el emisor no sabe quién lo escucha, el que escucha no sabe de dónde viene el evento — se comunican por el nombre del evento. La importación de Salim ya no necesita conocer el indicador: emite, el indicador escucha.
2. .on y .emit, más once y off
El funcionamiento se resume en dos métodos. .on(nombre, callback): REGISTRAR una escucha — «cuando se emita X, ejecuta esta función». .emit(nombre, ...datos): EMITIR — «dispara X con estos datos», todos sus escuchas son llamados entonces. Se crea el emisor con new EventEmitter() del módulo nativo events (o haciendo heredar una clase). También útiles: .once() (escuchar UNA sola vez, baja automática), .off() / .removeListener() (dejar de escuchar). Y regla de oro: el evento error es especial — emitido sin escucha, hace caer el programa. Salim escucha pues siempre los errores de su importación.
3. El modelo bajo todas las API de Node
Por qué es importante: es el modelo subyacente de muchas API — servidores HTTP (evento con cada petición), streams ('data', 'end'), sockets, y muchas bibliotecas exponen sus comportamientos vía eventos. Saber escuchar (.on('data', ...), .on('request', ...)) es constante en Node. El EventEmitter permite también una arquitectura desacoplada y extensible: añadir un comportamiento = añadir una escucha, sin tocar el emisor. No se crearán necesariamente muchos emisores personalizados al principio, pero se ENCONTRARÁN constantemente — de ahí la importancia del modelo emitir/escuchar.
🔎 Ejemplo concreto — 🔎 Ejemplo concreto — El progreso de la importación de Salim
Un emisor, dos escuchas independientes, como en casa de Salim:
const { EventEmitter } = require('node:events');
const progression = new EventEmitter();
progression.on('fichier', (nom) => console.log('Traité :', nom));
progression.on('termine', (total) => console.log('Import fini :', total));
progression.on('error', (err) => console.error('Échec import :', err.message));
progression.emit('fichier', 'rapport-01.txt');
progression.emit('termine', 1);
Lectura: la importación emite, la visualización y el registro escuchan sin conocerse.
Lección: la escucha error es obligatoria — si no, caída.
Vocabulario de la sección
- Programación por eventos
- Modelo donde unos objetos emiten eventos y otras partes del código reaccionan escuchando; desacoplamiento por el nombre del evento.
Ejemplo sencillo: La importación de Salim emite, el progreso y el registro escuchan.
Lo que no es: una llamada directa de función donde el llamante conoce al destinatario.
Reflejo: varias reacciones independientes = pensar en eventos.
- EventEmitter
- Clase nativa (módulo events) en el corazón del modelo de eventos de Node; muchos objetos nativos (servidor HTTP, streams) heredan de ella.
Ejemplo sencillo: Salim instancia new EventEmitter() para su progreso de importación.
Lo que no es: una Promise, que solo se resuelve una vez.
Reflejo: objeto Node que emite = probablemente un EventEmitter.
- .on() / .emit()
- on(evento, callback) registra una escucha; emit(evento, datos) dispara el evento y llama a todas sus escuchas.
Ejemplo sencillo: progression.on('fichier', ...) luego progression.emit('fichier', nom).
Lo que no es: un return de función que devuelve un valor al llamante.
Reflejo: .on antes de .emit, si no el evento se pierde.
- .once() / .off()
- once() escucha un evento una sola vez (baja automática); off()/removeListener() deja de escuchar.
Ejemplo sencillo: Salim escucha el evento de fin de importación con .once().
Lo que no es: una escucha que queda activa para siempre por defecto.
Reflejo: evento único esperado = .once().
- Evento error
- Evento especial: un emisor que emite error sin escucha hace caer el programa — escuchar siempre los errores.
Ejemplo sencillo: Salim añade progression.on('error', ...) desde la creación.
Lo que no es: un evento ordinario que se puede ignorar sin riesgo.
Reflejo: nuevo emisor = escucha error inmediata.
En la práctica — En la práctica — Utilizar la programación por eventos
- Cree un EventEmitter (const emitter = new EventEmitter()), registre una escucha con .on('event', callback) y emita con .emit('event', datos).
- Pase datos al emitir y recupérelos en la escucha; añada varias escuchas sobre el mismo evento.
- Pruebe .once() (escucha única) y escuche siempre el evento 'error' (si no, caída).
- Detecte que objetos nativos de Node son EventEmitters: escuche por ejemplo los eventos de un servidor HTTP o de un stream (.on('data', ...)).
Script listo para decir — explicar el desacoplamiento a un colega (30 s)
«Mi importación no conoce ni el indicador ni el registro: solo emite un evento por archivo tratado. Cada uno se suscribe con .on y reacciona a su manera. Si mañana añadimos una alerta por correo, añadimos una escucha — sin tocar una línea de la importación.»
Para decir mostrando la adición de una escucha en directo; es la demostración la que convence.
Modelo copiar-pegar — importación seguida por eventos
const { EventEmitter } = require('node:events');
class Importeur extends EventEmitter {
async importer(fichiers) {
let traites = 0;
for (const f of fichiers) {
try {
traites += 1;
this.emit('fichier', f, traites, fichiers.length);
} catch (err) {
this.emit('error', err);
return;
}
}
this.emit('termine', traites);
}
}
const importeur = new Importeur();
importeur.on('fichier', (f, n, total) => console.log(n + '/' + total + ' : ' + f));
importeur.once('termine', (total) => console.log('Import fini : ' + total + ' fichiers'));
importeur.on('error', (err) => console.error('Échec : ' + err.message));
importeur.importer(['rapport-01.txt', 'rapport-02.txt']);
Script en solitario
Una sola escucha de progreso basta: el emisor queda idéntico, se añaden escuchas cuando crece la necesidad.
Equipo en Argel
Salim añade una escucha que registra cada archivo en el archivo de seguimiento compartido — sin modificar la importación.
Evento único
Fin de importación, inicialización terminada: .once() evita las dobles reacciones si el evento se reemite.
Objeto nativo
En lugar de crear un emisor, escuchar el que existe: servidor HTTP, stream, process — .on('data'/'request'/'exit').
✓ Buenas prácticas
- Registre las escuchas (.on) antes de emitir, si no el evento se pierde.
- Escuche siempre error en cada emisor (si no, caída del programa).
- Añada un comportamiento añadiendo una escucha, no modificando el emisor.
✗ Errores frecuentes
- Usar una Promise para un flujo repetido (peticiones, trozos de stream).
- Olvidar la escucha error (caída al primer problema).
- Emitir antes de haber registrado las escuchas (eventos perdidos).
Puntos clave
- La PROGRAMACIÓN POR EVENTOS (en el corazón de Node): unos objetos EMITEN eventos, otros los ESCUCHAN y reaccionan — desacoplamiento por el nombre del evento.
- El EVENTEMITTER (módulo events) es la clase central; muchos objetos nativos heredan de ella (servidor HTTP, streams, sockets).
- Dos métodos clave: .on(evento, callback) (escuchar) y .emit(evento, datos) (disparar). También: .once() (una vez), .off() (dejar).
- Escuchar siempre el evento 'error' (emitido sin escucha = caída). Comprender el EventEmitter = comprender cómo interactuar con las API de Node.
Desafío 48h — aplique mañana
Transforme un tratamiento en bucle de su código en emisor: un evento por elemento tratado, un evento de fin en .once() y una escucha error. Añada luego una segunda escucha (registro, contador) sin tocar el emisor.
- Puedo explicar emitir/escuchar y el desacoplamiento por el nombre.
- Sé usar .on/.emit/.once/.off y escuchar error.
- He desacoplado un tratamiento real con dos escuchas independientes.
Preguntas frecuentes
¿Por qué la programación por eventos está tan presente en Node, y dónde se encuentra?
Porque encaja con la arquitectura asíncrona no bloqueante de Node: peticiones que llegan, datos de red que se presentan, lecturas que terminan — todo ocurre en momentos imprevisibles, y la reacción natural es «cuando llegue X, haz esto». Se encuentra en todas partes: el servidor HTTP emite con cada petición, los streams emiten 'data'/'end'/'error' (sección 2-3), los sockets señalan conexión y cierre, process emite 'exit', y los WebSockets (sección 4-3) viven de eventos. Como Salim, uno pasa el tiempo escuchando — de ahí la importancia del modelo.
¿Cuándo crear mi propio EventEmitter en lugar de usar callbacks o Promises?
Cuando el evento puede producirse varias veces y el desacoplamiento importa. Una Promise se resuelve una sola vez: perfecta para «haz esta operación y dame su resultado» (leer un archivo, consultar una base). Un EventEmitter emite tantas veces como sea necesario hacia cero, uno o varios escuchas independientes: cada petición de un servidor, cada trozo de un stream, cada archivo de la importación de Salim. Regla: un resultado único en relación directa = Promise/async-await; flujo repetido y consumidores desacoplados = EventEmitter.
Salim quiere reaccionar a cada archivo importado, tantas veces como sea necesario, con dos visualizaciones independientes. ¿Qué herramienta?