Llevas un tiempo trabajando con Node.js. Has construido algunas aplicaciones, jugado con diferentes módulos, e incluso te has sentido cómodo con la programación asíncrona. Pero hay algo que te ha estado molestando - el bucle de eventos.

Recuerda que si quieres aprender te dejo link al curso NodeJS

Si eres como yo, has pasado incontables horas leyendo documentación y viendo vídeos, intentando entender el bucle de eventos. Pero incluso siendo un desarrollador experimentado, puede ser difícil hacerse una idea completa de cómo funciona todo. Por eso he creado esta guía visual para ayudarte a entender completamente el bucle de eventos de Node.js. Siéntate, toma una taza de café, y vamos a sumergirnos en el mundo del bucle de eventos de Node.js.

Si tu quieres aprender más acerca de Qwik ve al siguiente video

Programación asíncrona en JavaScript


Comenzaremos con un repaso a la programación asíncrona en JavaScript. Aunque JavaScript encuentra su uso en aplicaciones web, móviles y de escritorio, es importante recordar que en su forma más básica, JavaScript es un lenguaje síncrono, de bloqueo y de un solo hilo. Vamos a entender esa línea con un breve fragmento de código.

// index.js

function A() {
  console.log("A");
}

function B() {
  console.log("B");
}

A()
B()

// Logs A and then B

JavaScript es síncrono


Si tenemos dos funciones que registran mensajes en la consola, el código se ejecuta de arriba abajo, con una sola línea ejecutándose en cada momento. En el fragmento de código, vemos que A se registra antes que B.

JavaScript es bloqueante


Debido a su naturaleza sincrónica. No importa lo que tarde un proceso anterior, los procesos posteriores no se iniciarán hasta que el primero se haya completado. En el fragmento de código, si la función A tiene que ejecutar un trozo intensivo de código, JavaScript tiene que terminarlo sin pasar a la función B. Incluso si ese código tarda 10 segundos o 1 minuto.

Es posible que hayas experimentado esto en el navegador. Cuando una aplicación web se ejecuta en un navegador y ejecuta un trozo intensivo de código sin devolver el control al navegador, éste puede parecer congelado. Esto se llama bloqueo. El navegador queda bloqueado y no puede seguir gestionando las entradas del usuario ni realizar otras tareas hasta que la aplicación web devuelve el control del procesador.

JavaScript es monohilo


Un hilo es simplemente un proceso que tu programa JavaScript puede utilizar para ejecutar una tarea. Y cada hilo sólo puede realizar una tarea a la vez. A diferencia de otros lenguajes que admiten multihilos y, por tanto, pueden ejecutar varias tareas en paralelo, JavaScript sólo tiene un hilo, denominado hilo principal, para ejecutar cualquier código.

Esperar en JavaScript


Como habrás adivinado, este modelo de JavaScript crea un problema porque tenemos que esperar a que se obtengan los datos antes de poder continuar con la ejecución del código. Esta espera puede durar varios segundos, durante los cuales no podemos ejecutar más código. Si JavaScript continúa sin esperar, nos encontraremos con un error. Necesitamos una forma de tener un comportamiento asíncrono en JavaScript. Entra Node.js.

Tiempo de ejecución de Node.js

El tiempo de ejecución de Node.js es un entorno en el que se puede utilizar y ejecutar un programa JavaScript fuera de un navegador. En su núcleo, el tiempo de ejecución de Node consta de tres componentes principales.

  • Dependencias externas: Como V8, libuv, crypto requeridas por Node.js para su funcionamiento.
  • Funciones C++: Que proporcionan funcionalidades como el acceso al sistema de archivos y a la red.
  • Una biblioteca JavaScript: Que proporciona funciones y utilidades para aprovechar las características de C++ desde el código JavaScript.
    Aunque todas las partes son importantes, el componente clave para la programación asíncrona en Node.js es la dependencia externa, libuv.

Libuv


Es una biblioteca multiplataforma de código abierto escrita en C. En el tiempo de ejecución de Node.js, su función es proporcionar soporte para el manejo de operaciones asíncronas. Veamos cómo funciona.

Ejecución de código en el tiempo de ejecución de Node.js

Vamos a conceptualizar cómo se ejecuta típicamente el código en el tiempo de ejecución de Node . Cuando ejecutamos código, el motor V8, situado a la izquierda de la imagen, se encarga de la ejecución del código JavaScript. El motor consta de un montón de memoria y una pila de llamadas.

Cada vez que declaramos variables o funciones, se asigna memoria al montón, y cada vez que ejecutamos código, las funciones se introducen en la pila de llamadas. Cuando una función regresa, se retira de la pila de llamadas. Esta es una implementación directa de la estructura de datos de la pila, donde el último elemento añadido es el primero en ser eliminado. A la derecha de la imagen, tenemos libuv, que es responsable del manejo de métodos asíncronos.

Cada vez que ejecutamos un método asíncrono, libuv se hace cargo de la ejecución de la tarea. Libuv entonces ejecuta la tarea utilizando mecanismos asíncronos nativos del sistema operativo. En caso de que los mecanismos nativos no estén disponibles o sean inadecuados, utiliza su pool de hilos para ejecutar la tarea, asegurándose de que el hilo principal no se bloquea.

Ejecución sincrónica de código


En primer lugar, echemos un vistazo a la ejecución sincrónica de código. El siguiente código consiste en tres sentencias log de consola que registran "Primero", "Segundo" y "Tercero" uno tras otro. Repasemos el código como si se estuviera ejecutando en tiempo de ejecución.

// index.js
console.log("First");
console.log("Second");
console.log("Third");

A continuación se muestra cómo se puede visualizar la ejecución de código síncrono con el tiempo de ejecución Node.

El hilo principal de ejecución siempre comienza en el ámbito global. La función global, si podemos llamarla así, se coloca en la pila. Luego, en la línea 1, tenemos una declaración de registro de consola. La función se coloca en la pila. Suponiendo que esto sucede en 1 ms, "Primero" se registra en la consola. Luego, la función es sacada de la pila.

La ejecución llega a la línea 3. Digamos que a los 2 ms, la función de registro se vuelve a colocar en la pila. "Segundo" se registra en la consola, y la función se retira de la pila.

Finalmente, la ejecución está en la línea 5. A los 3ms, la función es empujada a la pila, "Tercero" es registrado en la consola, y la función es sacada de la pila. No hay más código para ejecutar, y el global también es retirado.

// Dynamically render your components
export function MyPage({ json }) {
  return <BuilderComponent content={json} />
}

registerComponents([MyHero, MyProducts])

Ejecución de código asíncrono


A continuación, echemos un vistazo a la ejecución de código asíncrono. Consideremos el siguiente fragmento de código. Hay tres sentencias de registro, pero esta vez la segunda sentencia de registro está dentro de una función callback pasada a fs.readFile().

El hilo principal de ejecución siempre comienza en el ámbito global. La función global se coloca en la pila. La ejecución llega entonces a la línea 1. En 1ms, "Primero" se registra en la consola, y la función se retira de la pila. La ejecución pasa a la línea 3. A los 2ms, el método readFile es empujado a la pila. Como readFile es una operación asíncrona, se descarga a libuv.

JavaScript retira el método readFile de la pila de llamadas porque su trabajo ha terminado en lo que respecta a la ejecución de la línea 3. En segundo plano, libuv comienza a leer el contenido del archivo en un hilo separado. A los 3ms, JavaScript procede a la línea 7, empuja la función log a la pila, "Tercero" se registra en la consola, y la función sale de la pila.

Aproximadamente a los 4 ms, digamos que la tarea de lectura del archivo se ha completado en el grupo de hilos. La función callback asociada se ejecuta ahora en la pila de llamadas. Dentro de la función callback, se encuentra la sentencia log.

Esta se empuja a la pila de llamadas, "Second" se registra en la consola, y la función de registro se sale. Como no hay más sentencias que ejecutar en la función callback, ésta también se elimina. No hay más código que ejecutar, así que la función global también se elimina de la pila.

La salida de la consola va a leer "Primero", "Tercero", y luego "Segundo".

Libuv y las operaciones asíncronas


Está bastante claro que libuv ayuda a manejar operaciones asíncronas en Node.js. Para operaciones asíncronas como manejar una petición de red, libuv se apoya en las primitivas del sistema operativo. Para operaciones asíncronas como leer un archivo que no tiene soporte nativo del sistema operativo, libuv confía en su pool de hilos para asegurar que el hilo principal no se bloquea. Sin embargo, esto inspira algunas preguntas.

Cuando una tarea asíncrona se completa en libuv, ¿en qué momento decide Node ejecutar la función callback asociada en la pila de llamadas?


¿Espera Node a que la pila de llamadas esté vacía antes de ejecutar la función callback, o interrumpe el flujo normal de ejecución para ejecutar la función callback?


¿Qué ocurre con otros métodos asíncronos como setTimeout y setInterval, que también retrasan la ejecución de una función callback?


Si dos tareas asíncronas como setTimeout y readFile se completan al mismo tiempo, ¿cómo decide Node qué función callback ejecutar primero en la pila de llamadas? ¿Una tiene prioridad sobre la otra?

Todas estas preguntas pueden responderse entendiendo la parte central de libuv, que es el bucle de eventos.

¿Qué es el bucle de eventos?


Técnicamente, el bucle de eventos es sólo un programa C. Pero, puedes pensar en él como un patrón de diseño que orquesta o coordina la ejecución de código síncrono y asíncrono en Node.js.

Visualización del bucle de eventos


El bucle de eventos es un bucle que se ejecuta mientras tu aplicación Node.js está en marcha. Hay seis colas diferentes en cada bucle, cada una de las cuales contiene una o más funciones callback que necesitan ser ejecutadas en la pila de llamadas eventualmente.

En primer lugar, está la cola del temporizador (técnicamente un min-heap), que contiene callbacks asociados con setTimeout y setInterval.

En segundo lugar, está la cola I/O que contiene callbacks asociados con todos los métodos asíncronos como los métodos asociados con los módulos fs y http.

Tercero, está la cola check que contiene callbacks asociados con la función setImmediate, que es específica de Nod

.
En cuarto lugar, está la cola de cierre que contiene callbacks asociados con el evento de cierre de una tarea asíncrona.


Por último, está la cola microtask que contiene dos colas separadas.

La cola nextTick que contiene callbacks asociados con la función process.nextTick.


Promise que contiene las llamadas de retorno asociadas con la Promise nativa de JavaScript.


Es importante tener en cuenta que las colas timer, I/O, check y close forman parte de libuv. Las dos colas de microtarea, sin embargo, no forman parte de libuv. Sin embargo, siguen siendo parte del tiempo de ejecución de Node y juegan un papel importante en el orden de ejecución de las llamadas de retorno. Hablando de eso, vamos a entenderlo a continuación.

Cómo funciona el bucle de eventos


Las puntas de flecha ya nos delatan, pero es fácil confundirse. Déjame explicarte el orden de prioridad de las colas. En primer lugar, debes saber que todo el código JavaScript síncrono escrito por el usuario tiene prioridad sobre el código asíncrono que el tiempo de ejecución desea ejecutar. Esto significa que sólo cuando la pila de llamadas está vacía entra en juego el bucle de eventos.

Dentro del bucle de eventos, la secuencia de ejecución sigue ciertas reglas. Hay un buen número de reglas para envolver su cabeza alrededor, así que vamos a ir sobre ellos uno a la vez:

  • Se ejecutan todas las llamadas de retorno de la cola de microtareas. Primero, las tareas en la cola nextTick y sólo después las tareas en la cola promise.
  • Se ejecutan todas las devoluciones de llamada en la cola del temporizador.
    Las devoluciones de llamada en la cola de microtareas (si están presentes) se ejecutan después de cada devolución de llamada en la cola del temporizador.
  • Primero, las tareas en la cola nextTick, y luego las tareas en la cola promise.
  • Se ejecutan todas las llamadas de retorno de la cola de E/S.
  • Se ejecutan las callbacks en las colas de microtareas (si están presentes), comenzando con nextTickQueue y luego con la cola Promise.
  • Se ejecutan todas las callbacks en la cola de comprobación.
    Las devoluciones de llamada en las colas de microtareas (si están presentes) se ejecutan después de cada devolución de llamada en la cola de comprobación.
  • En primer lugar, las tareas de la cola nextTick y, a continuación, las tareas de la cola promise.
  • Se ejecutan todas las retrollamadas de la cola de cierre.
  • Por última vez en el mismo bucle, se ejecutan las colas de microtareas. Primero, las tareas en la cola nextTick, y luego las tareas en la cola promise.


Si hay más callbacks que procesar en este punto, el bucle se mantiene vivo para una ejecución más, y se repiten los mismos pasos. Por otro lado, si se ejecutan todas las retrollamadas y no hay más código que procesar, el bucle de eventos sale.

Este es el papel que juega el bucle de eventos de libuv en la ejecución de código asíncrono en Node.js. Con estas reglas en mente, podemos volver a las preguntas de antes.

Cuando una tarea asíncrona se completa en libuv, ¿en qué momento decide Node ejecutar la función callback asociada en la pila de llamadas?

Respuesta:

Las funciones callback se ejecutan sólo cuando la pila de llamadas está vacía.

¿Espera Node a que la pila de llamadas esté vacía antes de ejecutar la función callback, o interrumpe el flujo normal de ejecución para ejecutar la función callback?

Respuesta:

El flujo normal de ejecución no será interrumpido para ejecutar una función callback.

¿Qué ocurre con otros métodos asíncronos como setTimeout y setInterval, que también retrasan la ejecución de una función callback?

Respuesta:

Las retrollamadas setTimeout y setInterval tienen prioridad.

¿Qué ocurre con otros métodos asíncronos como setTimeout y setInterval, que también retrasan la ejecución de una función callback?

Respuesta:

Las devoluciones de llamada setTimeout y setInterval tienen prioridad.

Si dos tareas asíncronas como setTimeout y readFile se completan al mismo tiempo, ¿cómo decide Node qué función callback se ejecuta primero en la pila de llamadas? ¿Una tiene prioridad sobre la otra?

Respuesta:

Las callbacks de temporizador se ejecutan antes que las callbacks de E/S, incluso si ambas están listas exactamente al mismo tiempo.

Aprendimos mucho más, pero me gustaría que grabaras en tu mente esta representación visual (que es la misma que la anterior), ya que muestra cómo Node.js ejecuta código asíncrono bajo el capó.

"Pero espera, ¿dónde está el código que verifica esta visualización?", te preguntarás. Bueno, cada cola en el bucle de eventos tiene matices en la ejecución, por lo que es óptimo tratar con ellos de uno en uno. Esta entrada es la primera de una serie de entradas de blog sobre el bucle de eventos en Node.js.

Asegúrate de echar un vistazo a las otras partes enlazadas más abajo para entender algunas trampas que podrían hacerte tropezar, incluso con esta imagen impresa en tu mente.

Conclusión


El bucle de eventos es una parte fundamental de Node.js que permite la programación asíncrona asegurando que el hilo principal no se bloquee. Entender cómo funciona el bucle de eventos puede ser un reto, pero es esencial para construir aplicaciones de alto rendimiento.

Esta guía visual ha cubierto los fundamentos de la programación asíncrona en JavaScript, el tiempo de ejecución de Node.js, y libuv, que es responsable de la gestión de las operaciones asíncronas. Con este conocimiento, puedes construir un modelo mental sólido del bucle de eventos, que te ayudará a escribir código que aproveche la naturaleza asíncrona de Node.js.

Fuente