Manejo de eventos en Node.js con EventEmitter

En este tutorial, nos sumergiremos en la clase EventEmitter de Node para crear una aplicación emisora ​​de eventos. También lo extenderemos a una clase personalizada y cubriremos funciones importantes.

Introducción

En este tutorial, vamos a echar un vistazo a la clase EventEmitter nativa de Node. Aprenderá sobre eventos, qué puede hacer con un EvenEmitter y cómo aprovechar los eventos en su aplicación.

También cubriremos qué otros módulos nativos se extienden desde la clase EventEmitter y algunos ejemplos para comprender lo que sucede detrás de escena.

Entonces, en pocas palabras, cubriremos casi todo lo que necesita saber sobre la clase EventEmitter.

Usaremos algunas características básicas de ES6 como clases de JavaScript y funciones de flecha en este tutorial. Es útil, pero no obligatorio, si tiene algún conocimiento previo de la sintaxis de ES6.

¿Qué es un evento?

Todo un paradigma de software gira en torno a los eventos y su uso. La arquitectura basada en eventos es relativamente común hoy en día y las aplicaciones basadas en eventos producen, detectan y reaccionan a diferentes tipos de eventos.

Podría decirse que el núcleo de Node.js se basa en parte en eventos, ya que muchos módulos nativos, como el sistema de archivos (fs) y el módulo stream, están escritos como EventEmitters.

En la programación dirigida por eventos, un evento es el resultado de una o varias acciones. Esto puede ser una acción del usuario o una salida periódica de un sensor, por ejemplo.

Puede ver los programas basados ​​en eventos como modelos de publicación y suscripción en los que un editor desencadena eventos y los suscriptores los escuchan y actúan en consecuencia.

Por ejemplo, supongamos que tenemos un servidor de imágenes donde los usuarios pueden cargar imágenes. En la programación dirigida por eventos, una acción como cargar la imagen emitiría un evento. Para hacer uso de él, también habría 1..n suscriptores a ese evento.

Una vez que se activa el evento de carga, un suscriptor puede reaccionar enviando un correo electrónico al administrador del sitio web, informándole que un usuario ha cargado una foto. Otro suscriptor podría recopilar información sobre la acción y conservarla en la base de datos.

Estos eventos suelen ser independientes entre sí, aunque también pueden ser dependientes.

¿Qué es un EventEmitter?

La clase EventEmitter es una clase integrada que reside en el módulo eventos. Según la documentación:

Gran parte de la API central de Node.js se basa en una arquitectura idiomática asíncrona impulsada por eventos en la que ciertos tipos de objetos (llamados "emisores") emiten eventos con nombre que hacen que los objetos Función ("oyentes") ser llamado"

Esta clase puede, hasta cierto punto, describirse como una implementación auxiliar del modelo pub/sub, ya que ayuda a los emisores de eventos (editores) a publicar eventos (mensajes) y a los oyentes (suscriptores) a actuar sobre estos eventos. - de una manera sencilla.

Creación de emisores de eventos

Dicho esto, sigamos adelante y creemos un EventEmitter. Esto se puede hacer creando una instancia de la propia clase o implementándola a través de una clase personalizada y luego creando una instancia de esa clase.

Creación de un objeto EventEmitter

Comencemos con un objeto emisor de eventos simple. Crearemos un EventEmitter que emitirá un evento que contiene información sobre el tiempo de actividad de la aplicación, cada segundo.

Primero, importa la clase EventEmitter desde los módulos events:

1
const { EventEmitter } = require('events');

Entonces vamos a crear un EventEmitter:

1
const timerEventEmitter = new EventEmitter();

Publicar un evento desde este objeto es tan fácil como:

1
timerEventEmitter.emit("update");

Hemos especificado el nombre del evento y lo hemos publicado como evento. Sin embargo, no sucede nada ya que no hay un oyente que reaccione a este evento. Hagamos que este evento se repita cada segundo.

Usando el método setInterval(), se crea un temporizador que publicará el evento update cada segundo:

1
2
3
4
5
6
7
let currentTime = 0;

// This will trigger the update event each passing second
setInterval(() => {
    currentTime++;
    timerEventEmitter.emit('update', currentTime);
}, 1000);

La instancia EventEmitter acepta un nombre de evento y un conjunto arbitrario de argumentos. En este caso, hemos pasado eventName como update y currentTime como la hora desde el inicio de la aplicación.

Activamos el emisor mediante el método emit(), que envía el evento con la información que proporcionamos.

Con nuestro emisor de eventos listo, suscribamos un detector de eventos:

1
2
3
4
timerEventEmitter.on('update', (time) => {
    console.log('Message Received from publisher');
    console.log(`${time} seconds passed since the program started`);
});

Usando el método on(), pasando el nombre del evento para especificar a cuál nos gustaría adjuntar un oyente, nos permite crear oyentes. En el evento update, se ejecuta un método que registra la hora. Puede agregar el mismo oyente una y otra vez, y cada uno se suscribirá al evento.

El segundo argumento de la función on() es una devolución de llamada que puede aceptar cualquier número de datos adicionales emitidos por el evento. Cada oyente puede elegir qué datos quiere, una vez guardado el orden.

Ejecutar este script debería producir:

1
2
3
4
5
6
7
Message Received from publisher
1 seconds passed since the program started
Message Received from publisher
2 seconds passed since the program started
Message Received from publisher
3 seconds passed since the program started
...

Por el contrario, podemos usar el método once() para suscribirse, si necesita ejecutar algo solo la primera vez que se desencadena un evento:

1
2
3
4
timerEventEmitter.once('update', (time) => {
    console.log('Message Received from publisher');
    console.log(`${time} seconds passed since the program started`);
});

Ejecutar este código producirá:

1
2
Message Received from publisher
1 seconds passed since the program started

Emisor de eventos con múltiples oyentes

Ahora, hagamos un tipo diferente de emisor de eventos con tres oyentes. Esta será una cuenta regresiva. Un oyente actualizará al usuario cada segundo, un oyente notificará al usuario cuando la cuenta regresiva esté llegando a su fin y el último oyente se activará una vez que la cuenta regresiva haya terminado:

  • actualización - Este evento se activará cada segundo
  • end - Este evento se activará al final de la cuenta regresiva
  • end-soon: este evento se activará 2 segundos antes de que finalice la cuenta regresiva

Vamos a crear una función que cree este emisor de eventos y lo devuelva:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const countDown = (countdownTime) => {
    const eventEmitter = new EventEmitter();

    let currentTime = 0;

    // This will trigger the update event each passing second
    const timer = setInterval(() => {
        currentTime++;
        eventEmitter.emit('update', currentTime);

        // Check if countdown has reached to the end
        if (currentTime === countdownTime) {
            clearInterval(timer);
            eventEmitter.emit('end');
        }

        // Check if countdown will end in 2 seconds
        if (currentTime === countdownTime - 2) {
            eventEmitter.emit('end-soon');
        }
    }, 1000);
    return eventEmitter;
};

En esta función, hemos iniciado un evento basado en intervalos que emite el evento update en un intervalo de un segundo.

En la primera condición si, verificamos si la cuenta regresiva ha llegado al final y detenemos el evento basado en intervalos. Si es así, lanzamos un evento end.

En la segunda condición, verificamos si la cuenta regresiva está a 2 segundos de finalizar, y publicamos el evento end-soon si es así.

Ahora, agreguemos algunos suscriptores a este emisor de eventos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const myCountDown = countDown(5);

myCountDown.on('update', (t) => {
    console.log(`${t} seconds since the timer started`);
});

myCountDown.on('end', () => {
    console.log('Countdown is completed');
});

myCountDown.on('end-soon', () => {
    console.log('Count down will end in 2 seconds');
});

Este código debería producir:

1
2
3
4
5
6
7
1 seconds has been passed since the timer started
2 seconds has been passed since the timer started
3 seconds has been passed since the timer started
Count down will end in 2 seconds
4 seconds has been passed since the timer started
5 seconds has been passed since the timer started
Countdown is completed

Ampliación de EventEmitter

En esta sección, hagamos un emisor de eventos con la misma funcionalidad, extendiendo la clase EventEmitter. Primero, crea una clase CountDown que manejará los eventos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const { EventEmitter } = require('events');

class CountDown extends EventEmitter {
    constructor(countdownTime) {
        super();
        this.countdownTime = countdownTime;
        this.currentTime = 0;
    }

    startTimer() {
        const timer = setInterval(() => {
            this.currentTime++;
            this.emit('update', this.currentTime);
    
            // Check if countdown has reached to the end
            if (this.currentTime === this.countdownTime) {
                clearInterval(timer);
                this.emit('end');
            }
    
            // Check if countdown will end in 2 seconds
            if (this.currentTime === this.countdownTime - 2) {
                this.emit('end-soon');
            }
        }, 1000);
    }
}

Como puede ver, podemos usar this.emit() dentro de la clase directamente. Además, la función startTimer() se usa para permitirnos controlar cuándo comienza la cuenta regresiva. De lo contrario, se iniciaría tan pronto como se cree el objeto.

Vamos a crear un nuevo objeto de CountDown y suscríbete a él:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const myCountDown = new CountDown(5);

myCountDown.on('update', (t) => {
    console.log(`${t} seconds has been passed since the timer started`);
});

myCountDown.on('end', () => {
    console.log('Countdown is completed');
});

myCountDown.on('end-soon', () => {
    console.log('Count down will be end in 2 seconds');
});

myCountDown.startTimer();

Ejecutar esto resultará en:

1
2
3
4
5
6
7
1 seconds has been passed since the timer started
2 seconds has been passed since the timer started
3 seconds has been passed since the timer started
Count down will be end in 2 seconds
4 seconds has been passed since the timer started
5 seconds has been passed since the timer started
Countdown is completed

Un alias para la función on() es addListener(). Considere el detector de eventos end-soon:

1
2
3
myCountDown.on('end-soon', () => {
    console.log('Count down will be end in 2 seconds');
});

Podríamos haber hecho lo mismo con addListener() así:

1
2
3
myCountDown.addListener('end-soon', () => {
    console.log('Count down will be end in 2 seconds');
});

Ambos trabajan. Son casi como sinónimos. Sin embargo, la mayoría de los programadores prefieren usar on().

Funciones importantes de EventEmitter

Echemos un vistazo a algunas de las funciones importantes que podemos usar en EventEmitters.

nombres de eventos()

Esta función devolverá todos los nombres de oyentes activos como una matriz:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const myCountDown = new CountDown(5);

myCountDown.on('update', (t) => {
    console.log(`${t} seconds has been passed since the timer started`);
});

myCountDown.on('end', () => {
    console.log('Countdown is completed');
});

myCountDown.on('end-soon', () => {
    console.log('Count down will be end in 2 seconds');
});

console.log(myCountDown.eventNames());

Ejecutar este código dará como resultado:

1
[ 'update', 'end', 'end-soon' ]

Si tuviéramos que suscribirnos a otro evento como myCount.on('some-event', ...), el nuevo evento también se agregará a la matriz.

Tenga en cuenta que este método no devuelve los eventos publicados. Devuelve una lista de eventos que están suscritos a él.

removeListener()

Como sugiere el nombre, esta función elimina un controlador suscrito de un EventEmitter:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
const { EventEmitter } = require('events');

const emitter = new EventEmitter();

const f1 = () => {
    console.log('f1 Triggered');
}

const f2 = () => {
    console.log('f2 Triggered');
}

emitter.on('some-event', f1);
emitter.on('some-event', f2);

emitter.emit('some-event');

emitter.removeListener('some-event', f1);

emitter.emit('some-event');

Después de que se dispare el primer evento, dado que tanto f1 como f2 están activos, ambas funciones se ejecutarán. Después de eso, eliminamos f1 del EventEmitter. Cuando volvamos a emitir el evento, solo f2 se ejecutará:

1
2
3
f1 Triggered
f2 Triggered
f2 Triggered

Un alias para removeListener() es off(). Por ejemplo, podríamos haber escrito:

1
emitter.removeListener('some-event', f1);

Como:

1
emitter.off('some-event', f1);

Ambos tienen el mismo efecto.

removeAllListeners()

Una vez más, como sugiere el nombre, esta función eliminará a todos los oyentes de todos los eventos de un EventEmitter:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
const { EventEmitter } = require('events');

const emitter = new EventEmitter();

const f1 = () => {
    console.log('f1 Triggered');
}

const f2 = () => {
    console.log('f2 Triggered');
}

emitter.on('some-event', f1);
emitter.on('some-event', f2);

emitter.emit('some-event');

emitter.removeAllListeners();

emitter.emit('some-event');

El primer emit() disparará tanto f1 como f2 ya que están activos en ese momento. Después de eliminarlos, la función emit() emitirá el evento, pero ningún oyente responderá:

1
2
f1 Triggered
f2 Triggered

Gestión de errores {#gestión de errores}

Si desea emitir un error con su EventEmitter, debe hacerlo con un nombre de evento error. Esto es estándar para todos los objetos EventEmitter en Node.js. Este evento debe ir acompañado también de un objeto Error. Por ejemplo, se puede emitir un evento de error como este:

1
myEventEmitter.emit('error', new Error('Something bad happened'));

Cualquier oyente del evento error debe tener una devolución de llamada con un argumento para capturar el objeto Error y manejarlo correctamente. Si un EventEmitter emite un evento de error, pero no hay oyentes suscritos para los eventos de error, el programa Node.js lanzaría el Error que se emitió.

Esto finalmente detendrá la ejecución del proceso Node.js y saldrá de su programa, mientras muestra el seguimiento de pila del error en la consola.

Supongamos que, en nuestra clase CountDown, el parámetro countdownTime no puede comenzar siendo inferior a 2 porque, de lo contrario, no podremos activar el evento end-soon.

En tal caso, emitamos un evento de error:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class CountDown extends EventEmitter {
    constructor(countdownTime) {
        super();

        if (countdownTimer < 2) {
            this.emit('error', new Error('Value of the countdownTimer cannot be less than 2'));
        }

        this.countdownTime = countdownTime;
        this.currentTime = 0;
    }

    // ...........
}

El manejo de este error se maneja igual que otros eventos:

1
2
3
myCountDown.on('error', (err) => {
    console.error('There was an error:', err);
});

Se considera una buena práctica tener siempre un oyente para los eventos de error.

Módulos nativos usando EventEmitter

Muchos módulos nativos en Node.js amplían la clase EventEmitter y, por lo tanto, son emisores de eventos.

A great example is the Clase de corriente. The official documentation states:

Los flujos pueden ser legibles, escribibles o ambos. Todos los flujos son instancias de EventEmitter.

Echemos un vistazo a algunos usos clásicos de Stream:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const fs = require('fs');
const writer = fs.createWriteStream('example.txt');

for (let i = 0; i < 100; i++) {
  writer.write(`hello, #${i}!\n`);
}

writer.on('finish', () => {
  console.log('All writes are now complete.');
});

writer.end('This is the end\n');

Sin embargo, entre la operación de escritura y la llamada writer.end(), hemos agregado un oyente. Streams emite un evento finished al finalizar. Otros eventos, como error, pipe y unpipe se emiten cuando se produce un error o cuando un flujo de lectura se canaliza o se desconecta de un flujo de escritura.

Otra clase notable es la clase child_process y su método spawn():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const { spawn } = require('child_process');
const ls = spawn('ls', ['-lh', '/usr']);

ls.stdout.on('data', (data) => {
  console.log(`stdout: ${data}`);
});

ls.stderr.on('data', (data) => {
  console.error(`stderr: ${data}`);
});

ls.on('close', (code) => {
  console.log(`child process exited with code ${code}`);
});

Cuando child_process escribe en la canalización de salida estándar, se activará el evento data de stdout (que también extiende EventEmitter). Cuando el flujo de salida encuentra un error, el evento data se envía desde la tubería stderr.

Finalmente, después de que finaliza el proceso, se activa el evento close.

Conclusión

La arquitectura basada en eventos nos permite crear sistemas que están desacoplados pero altamente cohesivos. Los eventos representan el resultado de una determinada acción, y se pueden definir oyentes 1..n para escucharlos y reaccionar ante ellos.

En este artículo, nos hemos sumergido en la clase EventEmitter y su funcionalidad. Lo hemos instanciado y usado directamente, además de extender su comportamiento a un objeto personalizado.

Finalmente, hemos cubierto algunas funciones notables de la clase.

Como siempre, el código fuente está disponible en GitHub.