Hoy, en la sexta parte de la traducción del manual Node.js, hablaremos sobre el bucle de eventos, la pila de llamadas, la función
process.nextTick()
y los temporizadores. Comprender estos y otros mecanismos de Node.js es una de las piedras angulares del desarrollo exitoso de aplicaciones para esta plataforma.

[Le aconsejamos que lea] Otras partes del cicloParte 1:
Información general y primeros pasosParte 2:
JavaScript, V8, algunos trucos de desarrolloParte 3:
Hosting, REPL, trabajar con la consola, módulosParte 4:
archivos npm, package.json y package-lock.jsonParte 5:
npm y npxParte 6:
bucle de eventos, pila de llamadas, temporizadoresParte 7:
Programación asincrónicaParte 8:
Guía de Node.js, Parte 8: Protocolos HTTP y WebSocketParte 9:
Guía de Node.js, parte 9: trabajar con el sistema de archivosParte 10:
Guía de Node.js, Parte 10: Módulos estándar, flujos, bases de datos, NODE_ENVPDF completo de la guía Node.js Bucle de eventos
Si desea comprender cómo se ejecuta el código JavaScript, el bucle de eventos es uno de los conceptos más importantes que debe comprender. Aquí hablaremos sobre cómo funciona JavaScript en modo de subproceso único y cómo se manejan las funciones asincrónicas.
He estado desarrollando JavaScript durante muchos años, pero no puedo decir que entendí completamente cómo funciona todo, por así decirlo, "bajo el capó". El programador puede no ser consciente de las complejidades del dispositivo de los subsistemas internos del entorno en el que trabaja. Pero generalmente es útil tener al menos una idea general de tales cosas.
El código JavaScript que escribe se ejecuta en modo de subproceso único. En cierto momento, solo se realiza una acción. Esta limitación, de hecho, es muy útil. Esto simplifica enormemente la forma en que funcionan los programas, eliminando la necesidad de que los programadores resuelvan problemas específicos de entornos de subprocesos múltiples.
De hecho, un programador de JS solo debe prestar atención a las acciones exactas que realiza su código e intentar evitar situaciones que causen el bloqueo del hilo principal. Por ejemplo, hacer llamadas de red en modo síncrono y
ciclos interminables.
Por lo general, los navegadores, en cada pestaña abierta, tienen su propio bucle de eventos. Esto le permite ejecutar el código de cada página en un entorno aislado y evitar situaciones en las que una página determinada, en cuyo código hay un bucle infinito o se realizan cálculos pesados, puede "suspender" todo el navegador. El navegador admite el trabajo de muchos bucles de eventos existentes simultáneamente, que se utilizan, por ejemplo, para procesar llamadas a varias API. Además, se utiliza un bucle de eventos patentado para apoyar a
los trabajadores web .
Lo más importante que un programador de JavaScript debe recordar constantemente es que su código usa su propio bucle de eventos, por lo que el código debe escribirse para que este bucle de eventos no esté bloqueado.
Evento Loop Lock
Cualquier código de JavaScript que tarde demasiado tiempo en ejecutarse, es decir, el código que no tome el control del bucle de eventos durante demasiado tiempo, bloquea la ejecución de cualquier otro código de página. Esto incluso lleva a bloquear el procesamiento de los eventos de la interfaz de usuario, lo que se refleja en el hecho de que el usuario no puede interactuar con los elementos de la página y trabajar normalmente con ellos, por ejemplo, el desplazamiento.
Casi todos los mecanismos básicos de E / S de JavaScript son sin bloqueo. Esto se aplica tanto al navegador como a Node.js. Entre tales mecanismos, por ejemplo, podemos mencionar las herramientas para realizar solicitudes de red utilizadas en entornos de cliente y servidor, y herramientas para trabajar con archivos Node.js. Existen métodos sincrónicos para realizar tales operaciones, pero se usan solo en casos especiales. Es por eso que las devoluciones de llamada tradicionales y los mecanismos más nuevos, las promesas y la construcción asíncrona / espera, son de gran importancia en JavaScript.
Pila de llamadas
La pila de llamadas de JavaScript se basa en el principio LIFO (última entrada, primera salida - última entrada, primera salida). El bucle de eventos comprueba constantemente la pila de llamadas para ver si tiene una función que deba ejecutarse. Si, al ejecutar el código, se llama a una función, se agrega información sobre el mismo a la pila de llamadas y se ejecuta esta función.
Si incluso antes no estaba interesado en el concepto de una "pila de llamadas", entonces si ha encontrado mensajes de error que incluyen un seguimiento de la pila, ya se imagina cómo se ve. Aquí, por ejemplo, se ve así en un navegador.
Mensaje de error del navegadorEl navegador, cuando se produce un error, informa sobre la secuencia de llamadas a funciones, información sobre la cual se almacena en la pila de llamadas, lo que le permite encontrar la fuente del error y comprender qué llamadas a qué funciones condujeron a la situación.
Ahora que hemos hablado sobre el bucle de eventos y la pila de llamadas en términos generales, considere un ejemplo que ilustra la ejecución de un fragmento de código y cómo se ve este proceso en términos del bucle de eventos y la pila de llamadas.
Bucle de eventos y pila de llamadas
Aquí está el código con el que experimentaremos:
const bar = () => console.log('bar') const baz = () => console.log('baz') const foo = () => { console.log('foo') bar() baz() } foo()
Si se ejecuta este código, lo siguiente llegará a la consola:
foo bar baz
Tal resultado es bastante esperado. Es decir, cuando se ejecuta este código, primero se llama a la función
foo()
. Dentro de esta función, primero llamamos a la función
bar()
, y luego a la función
baz()
. Al mismo tiempo, la pila de llamadas durante la ejecución de este código sufre los cambios que se muestran en la siguiente figura.
Cambiar el estado de la pila de llamadas al ejecutar el código bajo investigaciónEl bucle de eventos, en cada iteración, verifica si hay algo en la pila de llamadas y, de ser así, lo hace hasta que la pila de llamadas esté vacía.
Iteraciones de bucle de eventosPoner en cola una función
El ejemplo anterior parece bastante ordinario, no tiene nada de especial: JavaScript encuentra el código que debe ejecutarse y lo ejecuta en orden. Hablaremos sobre cómo diferir la ejecución de la función hasta que se borre la pila de llamadas. Para hacer esto, se utiliza la siguiente construcción:
setTimeout(() => {}), 0)
Le permite ejecutar la función pasada a la función
setTimeout()
después de ejecutar todas las demás funciones llamadas en el código del programa.
Considere un ejemplo:
const bar = () => console.log('bar') const baz = () => console.log('baz') const foo = () => { console.log('foo') setTimeout(bar, 0) baz() } foo()
Lo que imprime este código puede parecer inesperado:
foo baz bar
Cuando ejecutamos este ejemplo, la función
foo()
se llama primero. En él, llamamos a
setTimeout()
, pasando esta función, como primer argumento,
bar
. Al pasarlo como segundo argumento, informamos al sistema que esta función debe realizarse lo antes posible. Luego llamamos a la función
baz()
.
Así es como se verá la pila de llamadas.
Cambiar el estado de la pila de llamadas al ejecutar el códigoAquí está el orden en que se ejecutarán las funciones en nuestro programa.
Iteraciones de bucle de eventos¿Por qué sucede esto de esta manera?
Cola de eventos
Cuando se llama a la función
setTimeout()
, el navegador o la plataforma Node.js inicia un temporizador. Después de que el temporizador funciona (en nuestro caso, esto sucede inmediatamente, ya que lo configuramos en 0), la función de devolución de llamada pasada a
setTimeout()
a la Cola de eventos.
La cola de eventos, cuando se trata del navegador, incluye eventos iniciados por el usuario: eventos causados por clics del mouse en elementos de la página, eventos que se activan cuando se ingresan datos desde el teclado. Los controladores de
onload
DOM como
onload
, funciones llamadas cuando se reciben respuestas a solicitudes asíncronas para cargar datos, están inmediatamente allí. Aquí están esperando su turno para procesar.
El bucle de eventos da prioridad a lo que hay en la pila de llamadas. Primero, hace todo lo que logra encontrar en la pila, y después de que la pila está vacía, procesa lo que está en la cola de eventos.
No necesitamos esperar hasta que una función como
setTimeout()
termine de funcionar, ya que el navegador proporciona funciones similares y utilizan sus propias transmisiones. Entonces, por ejemplo, al configurar el temporizador durante 2 segundos usando la función
setTimeout()
, no debe, después de haber detenido la ejecución de otro código, esperar estos 2 segundos, ya que el temporizador funciona fuera de su código.
ES6 Job Queue
ECMAScript 2015 (ES6) introdujo el concepto de Job Queue, que es utilizado por promesas (también aparecieron en ES6). Gracias a la cola de trabajos, el resultado de ejecutar la función asincrónica se puede utilizar lo más rápido posible, sin la necesidad de esperar a que se elimine la pila de llamadas.
Si se resuelve una promesa antes del final de la función actual, el código correspondiente se ejecutará inmediatamente después de que se complete la función actual.
Encontré una analogía interesante de lo que estamos hablando. Esto se puede comparar con una montaña rusa en un parque de diversiones. Después de montar la colina y volver a hacerlo, coges un boleto y te pones en la cola. Así es como funciona la cola de eventos. Pero la cola de trabajo se ve diferente. Este concepto es similar a un boleto de descuento, que le otorga el derecho de hacer el próximo viaje inmediatamente después de haber terminado el anterior.
Considere el siguiente ejemplo:
const bar = () => console.log('bar') const baz = () => console.log('baz') const foo = () => { console.log('foo') setTimeout(bar, 0) new Promise((resolve, reject) => resolve('should be right after baz, before bar') ).then(resolve => console.log(resolve)) baz() } foo()
Esto es lo que se generará después de su ejecución:
foo baz should be right after baz, before bar bar
Lo que puede ver aquí demuestra una gran diferencia entre las promesas (y la construcción asíncrona / en espera, que se basa en ellas) y las funciones asincrónicas tradicionales, cuya ejecución se organiza utilizando
setTimeout()
u otras API de la plataforma utilizada.
process.nextTick ()
El método
process.nextTick()
interactúa con el bucle de eventos de una manera especial. Una marca es un solo ciclo completo de eventos. Al pasar la función al método
process.nextTick()
, informamos al sistema que esta función debe llamarse después de que se complete la iteración actual del bucle de eventos, antes de que comience la siguiente. El uso de este método se ve así:
process.nextTick(() => {
Supongamos que un bucle de eventos está ocupado ejecutando código para la función actual. Cuando se complete esta operación, el motor de JavaScript ejecutará todas las funciones pasadas a
process.nextTick()
durante la operación anterior. Con este mecanismo, nos esforzamos por garantizar que una determinada función se ejecute de forma asincrónica (después de la función actual), pero lo antes posible, sin colocarla en la cola.
Por ejemplo, si usa la
setTimeout(() => {}, 0)
, la función se ejecutará en la próxima iteración del bucle de eventos, es decir, mucho más tarde que cuando se usa
process.nextTick()
en la misma situación. Este método debe usarse cuando sea necesario para garantizar la ejecución de algún código al comienzo de la próxima iteración del bucle de eventos.
setImmediate ()
Otra función proporcionada por Node.js para la ejecución de código asíncrono es
setImmediate()
. Aquí se explica cómo usarlo:
setImmediate(() => {
La función de devolución de llamada pasada a
setImmediate()
se ejecutará en la próxima iteración del bucle de eventos.
¿En qué se diferencia
setImmediate()
de
setTimeout(() => {}, 0)
(es decir, de un temporizador que debería funcionar lo antes posible) y de
process.nextTick()
?
La función pasada a
process.nextTick()
se ejecutará después de que se haya completado la iteración actual del bucle de eventos. Es decir, dicha función siempre se ejecutará antes de la función cuya ejecución se programa utilizando
setTimeout()
o
setImmediate()
.
Llamar a la función
setTimeout()
con un retraso establecido de 0 ms es muy similar a llamar a
setImmediate()
. El orden de ejecución de las funciones que se les transfieren depende de varios factores, pero en ambos casos las devoluciones de llamada se llamarán en la próxima iteración del bucle de eventos.
Temporizadores
Ya hemos hablado sobre la función
setTimeout()
, que le permite programar llamadas a las devoluciones de llamada que se le pasaron. Tomemos un tiempo para describir con más detalle sus características y considerar otra función,
setInterval()
, similar a ella. En Node.js, las funciones para trabajar con temporizadores están incluidas en el módulo de
temporizador , pero puede usarlas sin conectar este módulo en el código, ya que son globales.
▍ función setTimeout ()
Recuerde que cuando llama a la función
setTimeout()
, recibe una devolución de llamada y el tiempo, en milisegundos, después del cual se llamará la devolución de llamada. Considere un ejemplo:
setTimeout(() => { // 2 }, 2000) setTimeout(() => { // 50 }, 50)
Aquí pasamos
setTimeout()
nueva función que se describe inmediatamente, pero aquí podemos usar la función existente pasando
setTimeout()
su nombre y un conjunto de parámetros para ejecutarla. Se ve así:
const myFunction = (firstParam, secondParam) => {
La función
setTimeout()
devuelve un identificador de temporizador. Por lo general, no se usa, pero puede guardarlo y, si es necesario, eliminar el temporizador si ya no se necesita la devolución de llamada programada:
const id = setTimeout(() => {
▍ Cero retraso
En las secciones anteriores, usamos
setTimeout()
, pasándolo, como el tiempo después del cual es necesario llamar a la devolución de llamada,
0
. Esto significaba que la devolución de llamada se llamaría lo antes posible, pero después de completar la función actual:
setTimeout(() => { console.log('after ') }, 0) console.log(' before ')
Dicho código generará lo siguiente:
before after
Esta técnica es especialmente útil en situaciones en las que, al realizar tareas computacionales pesadas, no quisiera bloquear el hilo principal, permitiendo que se ejecuten otras funciones, dividiendo estas tareas en varias etapas, ejecutadas como llamadas
setTimeout()
.
Si recordamos la función
setImmediate()
, entonces es estándar en Node.js, lo que no se puede decir sobre los navegadores (se
implementa en IE y Edge, pero no en otros).
▍ función setInterval ()
La función
setInterval()
es similar a
setTimeout()
, pero hay diferencias entre ellas. En lugar de ejecutar la devolución de llamada que se le pasó una vez,
setInterval()
periódicamente, con el intervalo especificado, llamará a esta devolución de llamada. Esto continuará, idealmente, hasta el momento en que el programador detenga explícitamente este proceso. Aquí se explica cómo usar esta función:
setInterval(() => {
Una devolución de llamada pasada a la función que se muestra arriba se llamará cada 2 segundos. Para proporcionar la posibilidad de detener este proceso, debe obtener el identificador del temporizador devuelto por
setInterval()
y usar el
clearInterval()
:
const id = setInterval(() => { // 2 }, 2000) clearInterval(id)
Una técnica común es llamar a
clearInterval()
dentro de la devolución de llamada pasada a
setInterval()
cuando se cumple una determinada condición. Por ejemplo, el siguiente código se ejecutará periódicamente hasta que la propiedad
App.somethingIWait
esté
App.somethingIWait
para
arrived
:
const interval = setInterval(function() { if (App.somethingIWait === 'arrived') { clearInterval(interval) // - , - } }, 100)
▍ Configuración recursiva setTimeout ()
La función
setInterval()
llamará a la devolución de llamada que se le pasa cada
n
milisegundos, sin preocuparse de si esta devolución de llamada se ha completado después de su llamada anterior.
Si cada llamada a esta devolución de llamada siempre requiere el mismo tiempo menor que
n
, entonces no surgen problemas aquí.
Llamada periódica de devolución de llamada, cada sesión de ejecución de las cuales lleva el mismo tiempo, dentro del intervalo entre llamadasQuizás se necesita un tiempo diferente para completar una devolución de llamada, que aún es menor que
n
. Si, por ejemplo, estamos hablando de realizar ciertas operaciones de red, entonces esta situación es bastante esperada.
Llamada periódica de devolución de llamada, cada sesión de ejecución de las cuales toma un tiempo diferente, cayendo entre las llamadasCuando se usa
setInterval()
, puede surgir una situación cuando la devolución de llamada toma más de
n
, lo que lleva a que se complete la siguiente llamada antes de que se complete la anterior.
Llamada periódica llamada, cada sesión toma un tiempo diferente, que a veces no cabe en el intervalo entre llamadasPara evitar esta situación, puede usar la técnica de configuración del temporizador recursivo usando
setTimeout()
. El punto es que la próxima llamada de devolución de llamada se planifica después de la finalización de su llamada anterior:
const myFunction = () => {
Con este enfoque, se puede implementar el siguiente escenario:
Una llamada recursiva a setTimeout () para programar la ejecución de devolución de llamadaResumen
Hoy hablamos sobre los mecanismos internos de Node.js, como el bucle de eventos, la pila de llamadas y discutimos el trabajo con temporizadores que le permiten programar la ejecución del código. La próxima vez profundizaremos en el tema de la programación asincrónica.
Estimados lectores! ¿Ha encontrado situaciones en las que tuvo que usar process.nextTick ()?