2.2 · EventEmitter y programación por eventos

Nivel 2 · Núcleo de Node.js

2.2EventEmitter y programación por eventos

Objetivo: Comprender y utilizar el modelo de eventos de Node (emitir/escuchar vía EventEmitter, .on/.emit), central para interactuar con las API de Node.
Tiempo estimado: 10 min · lectura autónoma · Si solo tienes 5 min: lee «Lo esencial en 30s» + el esquema.

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.

Error frecuente de interpretación: «Callbacks, Promises y EventEmitter son lo mismo — usaré Promises para todo». No. Una Promise representa UN resultado único y se resuelve UNA vez: perfecta para «lee este archivo y dame su contenido». Un EventEmitter publica eventos que pueden producirse VARIAS veces (cada petición de un servidor, cada trozo de un stream) ante CERO, UNO o VARIOS escuchas independientes. Querer una Promise por petición recibida o por trozo de datos es usar la herramienta equivocada: flujo repetido y desacoplamiento = EventEmitter.
Emitir, escuchar, reaccionar Desacoplados por el nombre Emisor.emit(nombre, datos): ha pasado algo+Nombre de eventoEl único vínculo entre ambos lados+Escuchas.on(nombre, reacción): cada uno reacciona a su manera Añadir un comportamiento = añadir una escucha, sin tocar el emisor.
Desacoplados por el nombre
Versión en texto del esquema (SEO / lectores de pantalla / traducción): 1) La importación de Salim EMITE: un evento por archivo tratado, luego un evento de fin — no conoce a ningún destinatario. 2) El NOMBRE DE EVENTO hace el vínculo: el indicador y el registro se suscriben con .on, de forma independiente. 3) Cada uno REACCIONA a su manera: barra de progreso por un lado, línea de registro por otro. Lección: emitir/escuchar desacopla — y este modelo es el de los servidores, streams y sockets nativos.

🔎 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

  1. Cree un EventEmitter (const emitter = new EventEmitter()), registre una escucha con .on('event', callback) y emita con .emit('event', datos).
  2. Pase datos al emitir y recupérelos en la escucha; añada varias escuchas sobre el mismo evento.
  3. Pruebe .once() (escucha única) y escuche siempre el evento 'error' (si no, caída).
  4. Detecte que objetos nativos de Node son EventEmitters: escuche por ejemplo los eventos de un servidor HTTP o de un stream (.on('data', ...)).
Comprende y utiliza el modelo de eventos de Node (emitir/escuchar vía EventEmitter, .on/.emit), central para interactuar con las API de Node (servidores, streams…).

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.

Lista de salida de la sección
  • 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.
Autoevaluación 1-5: ¿desacoplo con eventos cuando hace falta?¿protejo cada emisor con una escucha error?

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.

Comprueba tu comprensión

Salim quiere reaccionar a cada archivo importado, tantas veces como sea necesario, con dos visualizaciones independientes. ¿Qué herramienta?

Recursos adicionales (opcional, para ir más allá)

Examen del nivel 2Preguntas inéditas y más difíciles para validar tus conocimientos