Patrones de EventEmitter
Usa Node.js EventEmitter para patrones de publicación-suscripción, con eventos tipados y manejadores asincronos.
Receta
Tarjeta de receta de referencia rápida — lista para copiar y pegar.
import { EventEmitter } from "node:events";
const emitter = new EventEmitter();
// Suscribirse
emitter.on("message", (text: string) => {
console.log("got:", text);
});
// Suscribirse una única vez
emitter.once("ready", () => {
console.log("ready fired once");
});
// Desuscribirse
const handler = (n: number) => console.log(n);
emitter.on("tick", handler);
emitter.off("tick", handler);
// Publicar
emitter.emit("message", "hello");
emitter.emit("ready");
// Siempre maneja errores — unhandled 'error' bloquea el proceso
emitter.on("error", (err) => {
console.error("emitter error:", err);
});
emitter.emit("error", new Error("boom"));Cuándo usarlo: Desacoplamiento de productores de consumidores dentro de un único proceso de Node — colas de trabajo, tuberías de registro, ciclo de vida de stream, hooks de plugin.
Ejemplo Práctico
Una clase envolvente de EventEmitter tipada para una cola de trabajo.
import { EventEmitter } from "node:events";
// Mapa de tipos: nombre de evento a tupla de argumentos
interface JobEvents {
"job:start": [id: string];
"job:progress": [id: string, percent: number];
"job:complete": [id: string, result: unknown];
"job:error": [id: string, err: Error];
}
class JobQueue extends EventEmitter {
override on<K extends keyof JobEvents>(
event: K,
listener: (...args: JobEvents[K]) => void
): this {
return super.on(event, listener as (...args: unknown[]) => void);
}
override emit<K extends keyof JobEvents>(
event: K,
...args: JobEvents[K]
): boolean {
return super.emit(event, ...args);
}
async run(id: string, task: (report: (p: number) => void) => Promise<unknown>) {
this.emit("job:start", id);
try {
const result = await task((percent) => {
this.emit("job:progress", id, percent);
});
this.emit("job:complete", id, result);
} catch (err) {
this.emit("job:error", id, err as Error);
}
}
}
const queue = new JobQueue();
queue.on("job:start", (id) => console.log(`[${id}] start`));
queue.on("job:progress", (id, pct) => console.log(`[${id}] ${pct}%`));
queue.on("job:complete", (id, result) => console.log(`[${id}] done`, result));
queue.on("job:error", (id, err) => console.error(`[${id}] failed`, err));
await queue.run("job-1", async (report) => {
for (let i = 0; i <= 100; i += 25) {
report(i);
await new Promise((r) => setTimeout(r, 50));
}
return { ok: true };
});Lo que esto demuestra:
- Las anulaciones tipadas para
onyemitproporcionan autocompletado total y seguridad de carga útil. - Los eventos de error son explícitos — sin fallos silenciosos.
- El productor (
run) y los consumidores (los manejadoreson) están completamente desacoplados.
Análisis Profundo
Cómo funciona
EventEmitter mantiene un mapa interno de nombre de evento a un array de funciones listener. emit itera ese array sincrónic amente, llamando a cada listener en el orden en que fue registrado. No hay cola incorporada ni contrapresión — los listeners se ejecutan en el tick actual a menos que ellos mismos programen trabajo asincrónico.
on añade, once añade un envolvente que se elimina a sí mismo después de la primera llamada, off (alias removeListener) elimina por referencia, y removeAllListeners(event?) borra todo.
El evento 'error' es especial: si nada está escuchando cuando haces emit('error', err), Node lanza el error y, si no se atrapa, termina el proceso.
Variaciones
EventEmitter tipado con generics. El patrón JobQueue anterior es el enfoque idiomático en Node + TypeScript modernos. Las bibliotecas como tsee o typed-emitter empaquetan la misma idea.
Iteración asincrónica con events.on(). Consume eventos como un stream:
import { on, EventEmitter } from "node:events";
const emitter = new EventEmitter();
setTimeout(() => emitter.emit("data", 1), 10);
setTimeout(() => emitter.emit("data", 2), 20);
for await (const [value] of on(emitter, "data")) {
console.log("value:", value);
if (value === 2) break;
}once como una Promise. Excelente para esperar un único evento del ciclo de vida:
import { once, EventEmitter } from "node:events";
const emitter = new EventEmitter();
setTimeout(() => emitter.emit("ready", "ok"), 50);
const [value] = await once(emitter, "ready");
console.log(value); // "ok"Extendiendo EventEmitter. Subclas ifica cuando el emitter es la identidad principal de tu objeto (el ejemplo JobQueue). Compón (ten un #emitter interno) cuando los eventos son una preocupación secundaria.
Eliminando todos los listeners. emitter.removeAllListeners() sin argumento elimina todos los eventos; con un argumento, solo ese evento. Útil en desmontaje pero peligroso si no posees el emitter.
Advertencia de máximo de listeners. Node imprime MaxListenersExceededWarning con 11 listeners por evento. Elévalo con emitter.setMaxListeners(50) o globalmente a través de EventEmitter.defaultMaxListeners. La advertencia es una pista de fuga, no un error.
Notas de TypeScript
Define un mapa de tipo de evento como una interfaz donde cada clave es el nombre del evento y cada valor es una tupla de los argumentos del listener — por ejemplo interface Events \{ start: [id: string]; progress: [percent: number] \}. Usa este mapa con una clase base genérica como TypedEmitter<Events> (de typed-emitter), o escribe tus propias anulaciones de on, once, off, y emit que restrinjan el nombre del evento con K extends keyof Events y los argumentos con Events[K]. Esto te da autocompletado en nombres de eventos y verificación de compilación de formas de carga útil.
Trampas
- Olvidar manejadores de
errorbloquea el proceso. Emitir'error'sin listener lanza. Siempre adjunta un listener deerrorantes de cualquier ruta de código que pueda emitir uno. - Advertencia de MaxListeners en 10. Agregar el mismo manejador en un bucle, o suscribirse dentro de un efecto de React sin limpieza, rápidamente excede el predeterminado. La advertencia se imprime una vez por emitter por evento — fácil de perder.
emites sincrónico. Los listeners se ejecutan en orden de registro en el mismo tick. Un listener sincrónico lento bloquea todos los demás listeners y el event loop.onceno previene otras registraciones. Solo envuelve ese único manejador. Otros listenersonsiguen disparándose para siempre.- Los listeners anónimos no se pueden eliminar.
emitter.off("x", () => {})pasa una referencia de función nueva y no elimina nada. Almacena el manejador en una variable si alguna vez necesitas eliminarlo. - Lanzar dentro de un listener. Un lanzamiento sincrónico se propaga desde
emity puede derribar listeners no relacionados. Envuelve trabajo riesgoso en try/catch o usa manejadores asincronos que rechacen en un evento deerror. - Los listeners asincronos son dispara y olvida.
emitno espera nada. Las promesas rechazadas en listeners se convierten en rechazos no manejados a menos que adjuntes.catchdentro del listener.
Alternativas
| Opción | Mejor Para | Notas |
|---|---|---|
node:events EventEmitter | Servicios solo de Node | Incorporado, cero dependencias, emit sincrónico. |
EventTarget / CustomEvent | Universal (Node 19+, navegadores, workers) | Estándar web, un poco más verboso, soporta AbortSignal para limpieza. |
mitt | Pub-sub universal minúsculo | ~200 bytes, sin comodín, sin once — solo on/off/emit. |
nanoevents | Emitter tipado pequeño | Genéricos de TypeScript de primera clase, devuelve una función de desuscripción. |
RxJS Subject | Streams, operadores, contrapresión | Pesado, pero inigualable para tuberías asincrónicas complejas. |
| Streams de Node | Streams de bytes/objetos con contrapresión | Usa cuando el orden y el control de flujo importan, no solo señalización. |
Preguntas Frecuentes
¿Qué pasa si emito un evento error sin un listener?
Node lanza el error sincrónic amente desde emit. Si nada lo atrapa, el proceso se bloquea con un mensaje Unhandled 'error' event. Siempre adjunta un listener de error antes del primer emit.
¿Es emit sincrónico o asincrónico?
Sincrónico. Los listeners se ejecutan en orden de registro en el tick actual, bloqueando el event loop hasta que regresan. Los listeners asincronos son dispara y olvida — emit no los espera.
¿Cómo elimino un listener que agregué con once?
Guarda la referencia y pásala a off: const h = () => {}; emitter.once("x", h); emitter.off("x", h). También puedes llamar a removeAllListeners("x") si posees el emitter.
¿Por qué Node advierte sobre 11 listeners?
MaxListenersExceededWarning se dispara con 11 listeners en un único evento — una heurística para detectar fugas. Si realmente necesitas más, llama a emitter.setMaxListeners(50) o establece EventEmitter.defaultMaxListeners globalmente.
¿Cómo tipificar eventos en TypeScript sin una biblioteca? (TypeScript)
Define una interfaz mapeando nombres de eventos a tuplas de argumentos, luego subclas ifica EventEmitter y anula on y emit con firmas genéricas restringidas por K extends keyof YourEvents. Ve el ejemplo práctico de JobQueue en esta página.
¿Debo extender EventEmitter o componerlo? (TypeScript)
Extiende cuando los eventos son la identidad principal del objeto (un bus, una cola, un stream). Compón con un campo privado #emitter cuando los eventos son una preocupación secundaria — eso mantiene tu API pública más pequeña y evita exponer todos los métodos de EventEmitter.
¿Puedo esperar un evento?
Sí — usa events.once(emitter, "name") que devuelve una promise que se resuelve a los argumentos emitidos como un array. También rechaza en 'error', lo que la hace más segura que un envolvente hecho a mano.
¿Cómo consumo eventos como un stream?
Usa events.on(emitter, "name") que devuelve un iterador asincrónico. Puedes hacer for await sobre él y break para dejar de escuchar. Empareja con una opción AbortSignal para cancelación limpia.
¿Cuál es la diferencia entre off y removeListener?
Ninguna — off es un alias agregado para paridad con el DOM. Ambos eliminan un listener por referencia.
¿Por qué mi listener anónimo sigue disparándose después de intentar eliminarlo? (Gotcha)
emitter.off("x", () => {}) crea una función nueva y la pasa a off, que no encuentra coincidencia. Almacena el manejador en una variable o usa once si solo lo necesitas una vez.
¿Qué debo usar en lugar de EventEmitter en el navegador? (Gotcha)
EventTarget con CustomEvent es el estándar web y funciona en todas partes, incluido Node 19+. Para pub-sub universal ultra pequeño, alcanza mitt o nanoevents. No envíes node:events al navegador.
¿Cómo prevengo rechazos no manejados de listeners asincronos?
Envuelve el cuerpo asincrónico en try/catch y reenvía fallos al emitter: emitter.on("x", async (v) => { try { await work(v); } catch (err) { emitter.emit("error", err); } }). emit no esperará tu manejador, así que debes atrapar adentro.
Relacionado
docs/nodejs-scripts/http-server.md— Servidores HTTP, que son instancias deEventEmitter.docs/browser-apis/postmessage-basics.md— mensajería entre ventanas, un patrón pub-sub del lado del navegador.docs/react-hooks/use-effect.md— suscripción y limpieza de listeners de eventos desde React.