Cómo usar temporizadores y eventos en Node.js

Node.js tiene múltiples utilidades para manejar eventos y programar la ejecución del código. Estas utilidades, combinadas, le brindan la capacidad de reactivar...

Eventos y temporizadores en Node.js

Node.js tiene múltiples utilidades para manejar eventos y programar la ejecución del código. Estas utilidades, combinadas, le brindan la capacidad de responder de manera reactiva en el momento adecuado, por ejemplo:

  • Borrar los datos de la sesión cuando un usuario cierra la sesión
  • Programar un tiempo de espera para recibir resultados de una llamada API y especificar el código de manejo de errores para ejecutar en caso de tiempo de espera
  • Cerrar las conexiones de la base de datos antes de salir de Node.js

En este artículo, veremos cómo funcionan los temporizadores en Node.js. También presentaremos cómo funciona el ciclo de eventos de Node.js y cómo puede aprovechar las capacidades de manejo de eventos de Node.

Temporizadores

El primer conjunto de utilidades que veremos son las utilidades de temporización setTimeout, setImmediate y setInterval. Con estas herramientas, podemos controlar el tiempo de ejecución del código.

¿Por qué sería esto importante? En Node.js, al igual que cuando se usan otros lenguajes de programación como C, Python, Java y otros, es útil programar ciertas funciones para que se ejecuten repetidamente.

Supongamos, por ejemplo, que queremos copiar ciertos archivos desde una ubicación de recepción a un archivo permanente. Este sería un buen escenario para programar una transferencia de archivos. En ciertos intervalos, podemos verificar si hay nuevos archivos y luego copiarlos en la ubicación de la copia de seguridad, si los hay.

establecer tiempo de espera

Con setTimeout, podemos programar el código para que se ejecute después de que haya pasado un cierto período de tiempo.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// setTimeout.js

let cue = 'The actors are here!';

// However, the cue is not announced until at least 5000ms have
// passed through the use of setTimeout
setTimeout(function() {
    return console.log(cue);
}, 5000);

// This console log is executed right away
console.log('An exploration of art and music. And now, as we wait for the actors...');

Para ejecutar este código y verlo en acción, ejecuta node setTimeout.js en tu terminal:

1
2
3
$ node setTimeout.js
An exploration of art and music. And now, as we wait for the actors...
The actors are here!

Observe cómo aunque la llamada console('Una exploración...') es después de nuestra llamada console.log(cue), todavía se ejecuta primero.

El truco para darse cuenta aquí es que solo se garantiza que el código se ejecutará después de que haya pasado al menos ese período de tiempo, no justo en el punto.

establecerIntervalo

En situaciones en las que necesite una ejecución de código regular y repetida, como un sondeo prolongado, el método setInterval será más adecuado que setTimeout. Con esta función, podemos especificar una función para que se ejecute cada X segundos. La función en realidad toma su argumento en milisegundos, por lo que debe hacer la conversión usted mismo antes de ingresar sus argumentos.

Supongamos que queremos verificar la longitud de la cola en un autoservicio de McDonald’s para que los usuarios de nuestro programa puedan salir corriendo en el mejor momento. Usando setInterval, podemos verificar repetidamente la longitud de la cola y decirles cuándo la costa está despejada.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// setInterval.js

// This function simulates us checking the length
// of a McDonald's drive-through queue
let getQueueLength = function() {
    return Math.round(12 * Math.random());
};

// We would like to retrieve the queue length at regular intervals
// this way, we can decide when to make a quick dash over
// at the optimal time
setInterval(function() {
    let queueLength = getQueueLength();

    console.log(`The queue at the McDonald's drive-through is now ${queueLength} cars long.`);

    if (queueLength === 0) {
        console.log('Quick, grab your coat!');
    }

    if (queueLength > 8) {
        return console.log('This is beginning to look impossible!');
    }
}, 3000);

Puedes ver la salida a continuación. Ejecute el código con node setInterval.js, como se muestra a continuación:.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ node setTimeout.js 
The queue at the McDonald's drive-through is now 6 cars long.
The queue at the McDonald's drive-through is now 0 cars long.
Quick, grab your coat!
The queue at the McDonald's drive-through is now 1 cars long.
The queue at the McDonald's drive-through is now 3 cars long.
The queue at the McDonald's drive-through is now 9 cars long.
This is beginning to look impossible!
The queue at the McDonald's drive-through is now 0 cars long.
Quick, grab your coat!
The queue at the McDonald's drive-through is now 10 cars long.
This is beginning to look impossible!

establecerInmediato

Si queremos que una función se ejecute con la mayor urgencia posible, usamos setImmediate. La función que ejecutamos de esta manera se ejecutará antes de todas las llamadas setTimeout o setInterval tan pronto como el bucle de eventos actual de Node.js haya terminado de llamar a las devoluciones de llamadas de eventos.

Aquí hay un ejemplo de esto en proceso. Puede ejecutar este código con el comando node setImmediate.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// setImmediate.js

// A timeout
setTimeout(function() {
    console.log('I am a timeout');
}, 5000);

// An interval
setInterval(function() {
    console.log('I am an interval');
}, 5000);

// An immediate, its callback will be executed before those defined above
setImmediate(function() {
    console.log('I am an immediate');
});

// IO callbacks and code in the normal event loop runs before the timers
console.log('I am a normal statement in the event loop, guess what comes next?');
1
2
3
4
5
6
7
8
$ node setImmediate.js 
I am a normal statement in the event loop, guess what comes next?
I am an immediate
I am a timeout
I am an interval
I am an interval
I am an interval
...

La devolución de llamada setImmediate, aunque definida después de setInterval y setTimeout, se ejecutará antes que ellos.

El bucle de eventos

Una pregunta que se le podría haber ocurrido es "¿Cómo realiza Node.js un seguimiento de todos estos tiempos, temporizadores y eventos? ¿Cómo se prioriza el orden de ejecución?" Esta es una buena línea de investigación y requiere mirar algo conocido como "Node.js Event Loop".

Entonces, ¿qué es el bucle de eventos?

El Bucle de eventos es simplemente un ciclo repetido por el cual Node.js cambia a través del procesamiento de cálculos. Dado que no puede realizar todos los cálculos posibles simultáneamente, al ser de un solo subproceso, cambia de cálculo a cálculo en un bucle bien definido conocido como bucle de eventos.

El Event Loop tiene las siguientes etapas básicas:

  • Temporizadores: ejecuta devoluciones de llamada que se han programado con setTimeout y setInterval
  • Devoluciones de llamada pendientes: ejecuta cualquier devolución de llamada que esté lista para ejecutarse
  • Inactivo, preparar - interno a Node.js
  • Encuesta: acepta conexiones entrantes y procesamiento de datos
  • Verificar: invoca las devoluciones de llamada establecidas mediante setImmediate
  • Cerrar devoluciones de llamada: ejecuta devoluciones de llamada para cerrar eventos

El bucle de eventos es la columna vertebral del trabajo con eventos y otras devoluciones de llamadas asincrónicas en Node.js. Nos permite colocar ganchos en determinados puntos que serán golpeados en el transcurso del bucle.

Respuesta a devoluciones asincrónicas con devoluciones de llamadas

Dada la naturaleza de subproceso único de Node.js, las operaciones de ejecución prolongada, como lecturas de archivos o consultas de bases de datos, se descargan rápidamente en el sistema operativo; luego, Node.js continúa con su ciclo de eventos normalmente. Esto mantiene las cosas eficientes y rápidas.

¿Cómo interactúan esos procesos del sistema operativo con el proceso de Node.js? Por medio de callbacks. Usamos una devolución de llamada para procesar cosas de forma asíncrona en segundo plano, luego nos conectamos de nuevo al bucle de eventos una vez que se ha completado el trabajo asíncrono. Para obtener este tipo de funcionalidad en otros lenguajes de programación, puede usar una cola de tareas como Apio en Python o Sidekiq en Rubí. En Node.js, debido a que Event Loop y la ejecución asíncrona de Node.js ya ponen las cosas en cola automáticamente, obtiene este procesamiento asíncrono de forma gratuita.

Para ver las devoluciones de llamada en acción, vamos a leer un archivo del sistema de archivos y usar una devolución de llamada para imprimir el contenido.

El primer paso es crear el archivo. En este caso, estamos usando un archivo de texto que contiene las líneas de un poema de T.S. Eliot. Puede sustituir su propio archivo. Este archivo se llama poem.txt y puede colocar los siguientes contenidos en él.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// poem.txt

Macavity - The Mystery Cat, by T. S. Eliot

Macavity's a Mystery Cat: he's called the Hidden Paw--
For he's the master criminal who can defy the Law.
He's the bafflement of Scotland Yard, the Flying Squad's despair:
For when they reach the scene of crime--Macavity's not there!

Macavity, Macavity, there's no on like Macavity,
He's broken every human law, he breaks the law of gravity.
His powers of levitation would make a fakir stare,
And when you reach the scene of crime--Macavity's not there!
You may seek him in the basement, you may look up in the air--
But I tell you once and once again, Macavity's not there!

En el mismo directorio, crearemos nuestro script que leerá este archivo de poema y lo imprimirá nuevamente. La impresión del archivo o el manejo de un error se realizarán en una devolución de llamada, después de que el sistema operativo devuelva el resultado de la lectura del archivo. Como se muestra a continuación, en readFile.js, su devolución de llamada se activa después de que regresa el proceso del sistema operativo asíncrono. Cuando este proceso del sistema operativo regresa, la devolución de llamada de Node.js que proporcionó se coloca en el bucle de eventos para que se procese, que luego se ejecutará cuando el bucle llegue a ese proceso.

Su devolución de llamada puede hacer cualquier cosa, desde actualizar el estado en la aplicación hasta manejar un error, si lo hay, y cerrar la sesión del usuario, no hacer nada o incluso terminar el proceso de Nodo por completo.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// readFile.js

const fs = require('fs');

// Attempt to read the poem file
// Attach a callback to handle a successful read and print the contents to console
fs.readFile('./poem.txt', 'utf-8', function(err, data) {
    if (err) return console.error(err);

    let poem = data.toString();
    console.log('Here is the poem of the day...\n\n');
    return console.log(data);
});

Ejecute este código con node readFile.js. Se leerá el archivo y la consola debería imprimirle el poema. De lo contrario, imprimirá el error encontrado, por ejemplo, si no existe dicho archivo en la ruta especificada.

Las devoluciones de llamada son adecuadas para el manejo único de datos, errores y eventos. Sin embargo, las devoluciones de llamada pueden complicarse cuando se anidan en varios niveles de profundidad. Otra forma alternativa de manejar eventos es usar detectores de eventos, que se tratan en la siguiente sección.

Responder a eventos con escuchas de eventos

Los detectores de eventos son funciones que se ejecutan cuando ocurren tipos de eventos específicos. Por ejemplo, al leer un archivo, establecer una conexión con el servidor o consultar una base de datos, los módulos que utilizamos, como fs, net o mongoose, tienen tipos de eventos incorporados que emitir.

Los objetos que normalmente emiten estos eventos amplían el objeto base EventEmitter, que proviene del módulo eventos integrado.

Su aplicación puede responder a estos eventos a través del mecanismo de detectores de eventos. Por lo general, adjunta un detector de eventos en el código a través de la palabra clave "on", seguida de una cadena que especifica el tipo de evento y, finalmente, una función, que es el código que se ejecutará cuando ocurra el evento.

Para ver los detectores de eventos en acción, vamos a crear un servidor que interactúe con una API Cat y analice las respuestas de la API. Nuestro servidor atenderá las solicitudes y mostrará a los visitantes una imagen de "Gato del día". Los eventos con los que trabajaremos son parte del módulo http.

También usaremos un módulo xml2js para analizar las respuestas XML que produce Cat API. Para instalar xml2js, querrá ejecutar el comando npm install xml2js en un directorio de proyecto adecuado.

Una vez que haya instalado el módulo, cree dos archivos en el directorio, cats.html y cats.js. Dentro de cats.html, coloque el front-end de nuestra aplicación. Esto simplemente mostrará los datos del gato que vamos a analizar.

 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<!-- cats.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>Cats</title>

    <!-- Bootstrap -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css">

    <!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
    <!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
    <!--[if lt IE 9]>
      <script src="https://oss.maxcdn.com/libs/html5shiv/3.7.2/html5shiv.js"></script>
      <script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
    <![endif]-->
  </head>
  <body>

    <div class="container-fluid">
      <div class="col-md-8 col-md-offset-2">
        <h1>Cats Of Silicon Valley</h1>

        <h2>Welcome to the Cat Of The Day</h2>

        <img src=IMGSRC class="img-fluid" alt="Responsive image">
        <br>
        <label class="primary">Source: SOURCE</label>
        <br>
        <a href="/" class="btn btn-primary btn-lg">More Cats!</a>
      </div>
    </div>

    <!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
    <!-- Include all compiled plugins (below), or include individual files as needed -->
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
  </body>
</html>

La mayor parte de nuestra lógica estará en el código del lado del servidor que funciona con los detectores de eventos. Esto está contenido en el archivo cats.js. Para ver el código del detector de eventos en acción, coloque el siguiente código dentro del archivo, luego ejecútelo con node cats.js y, en su navegador, visite http://localhost:4000.

 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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// cat.js

const http = require('http');
const fs = require('fs');
const xml2js = require('xml2js');

// We will get images from the CatAPI https://thecatapi.com/
let catApi = 'http://thecatapi.com/api/images/get?format=xml&results_per_page=1';

let catUrl = '';
let catSource = '';

let server = http.createServer(function(req, res) {
    // Get fresh cat data from the Cat API
    http.get(catApi, (res) => {
        let data = '';
    
        // Attach event listener for when receiving data from the remote server is complete
        res.on('end', () => {
            console.log('***We have completed cat data\n***');
            console.log(data);
      
            let parser = new xml2js.Parser();
            return parser.parseString(data, function(err, imgxml) {
                if (err) {
                    return console.log('Error parsing cat data');
                } else {
                    let imgjson = JSON.parse(JSON.stringify(imgxml));
        
                    console.log('***We have cat JSON***');
                    console.log(imgjson);
        
                    catUrl = imgjson.response.data[0].images[0].image[0].url[0];
                    return catSource = imgjson.response.data[0].images[0].image[0].source_url[0];
                }
            });
        });
    
        // Event listener for the 'data' event
        // In this case, accumulate all the data so we can use it all at once later
        return res.on('data', (xml) => {
            return data += xml;
        });
    });

    // Serve cat images from the CatAPI
    return fs.readFile('./cats.html', function(err, cathtml) {
        if (err) {
            console.error(err);
            return res.end('An error occurred');
        }
    
        let html = cathtml.toString()
                          .replace('IMGSRC', catUrl)
                          .replace('SOURCE', catSource);
    
        res.writeHead(200, {
            'Content-Type': 'text/html'
        });
    
        res.write(html);
        return res.end();
    });
});

// Run the server
server.listen(4000);

A continuación, entramos en el código en detalle. También eche un vistazo a los comentarios en el código.

Como puede ver en el código, nuestra solicitud a la API de Cat solicita nuevos datos de cat. Luego dejamos que la ejecución de Node.js continúe normalmente. Sin embargo, adjuntamos dos detectores de eventos para tratar los nuevos eventos de la API remota. El primero de ellos es un detector de eventos "on end". Cuando tenemos una carga útil cat completa de la API Cat, actualizamos nuestra página con los nuevos datos e imágenes. La segunda clase de evento que estamos escuchando es el evento "datos". Esto se activa cuando hay nuevos datos del host remoto. En ese caso, almacenamos los datos en un búfer y los agregamos a nuestro almacén de datos temporal.

Ahora, gracias al poder de los detectores de eventos, hemos facilitado la obtención de nuevas imágenes de gatos a voluntad.

Gato del día 1{.img-responsive}

Los visitantes de nuestro sitio web pueden obtener nuevas imágenes del gato del día con solo hacer clic en un botón.

Gato del día 2{.img-responsive}

Hay mucho más en eventos y temporizadores en Node.js que lo que describimos aquí. Un buen próximo tema para explorar es el de los emisores de eventos, que le brindan aún más poder sobre los tipos de eventos que su aplicación puede utilizar.