Mecanismos de Ejecución de JavaScript

Consideremos el siguiente fragmento de código JavaScript:

console.log('Inicio');
setTimeout(function(){
    console.log('Tarea asíncrona');
},0);
console.log('Fin');

¿Cuál sería el resultado impreso? Aunque parece sencillo, sin comprender el mecanismo de ejecución de JavaScript es fácil equivocarse. La salida correcta es: Inicio Fin Tarea asíncrona

I: Fundamentos del Ejecutor de JavaScript

Para entender el mecanismo de ejecución de JavaScript, primero debemos conocer algunos conceptos clave:

1. El concepto de JavaScript de un solo hilo

JavaScript es conocido por ser de un solo hilo, lo que significa que solo puede realizar una tarea a la vez. Este diseño está relacionado con su propósito principal como lenguaje de scripting para navegadores: interactuar con usuarios y manipular el DOM. Si JavaScript tuviera múltiples hilos, surgirían problemas de sincronización complejos. Por esta razón, desde su origen, JavaScript ha sido de un solo hilo, una característica central que no cambiará.

2. Tareas síncronas y asíncronas en JavaScript

El modelo de un solo hilo implica que todas las tareas deben encolarse. Una tarea debe completarse antes de que la siguiente pueda comenzar. Si una tarea consume mucho tiempo, las siguientes deben esperar. Los diseñadores de JavaScript abordaron este problema dividiendo todas las tareas en dos categorías: tareas síncronas y tareas asíncronas.

Las tareas síncronas son: aquellas que se ejecutan en el hilo principal, en orden secuencial. Solo cuando una tarea termina, la siguiente puede comenzar.

Las tareas asíncronas son: aquellas que no entran directamente en el hilo principal, sino que se registran en una "cola de tareas" (task queue). Solo cuando la cola de tareas notifica al hilo principal que una tarea asíncrona está lista para ejecutarse, esta pasa al hilo principal.

Las tareas asíncronas incluyen: temporizadores, solicitudes de red, Promesas, carga de imáganes

El flujo de ejecución de tareas síncronas y asíncronas en JavaScript es el siguiente:

  1. Todas las tareas síncronas se ejecutan en el hilo principal, formando una pila de ejecución.
  2. Las tareas asíncronas entran en una Tabla de Eventos y registran sus funciones. Cuando una tarea asíncrona obtiene un resultado, la Tabla de Eventos mueve su función a una Cola de Eventos.
  3. Cuando el hilo principal queda libre (sin tareas pendientes), el sistema lee la Cola de Eventos en orden y las tareas asíncronas pasan a la pila de ejecución para comenzar su ejecución.
  4. El hilo principal repite constantemente el paso 3. Debido a que este proceso es cíclico, se conoce como Bucle de Eventos (Event Loop).

3. Tareas macro y micro en JavaScript

Además de las tareas síncronas y asíncronas, JavaScript clasifica las tareas de manera más específica: tareas macro (macro-task) y tareas micro (micro-task).

Tareas macro:

  1. Las tareas macro son definidas por los navegadores: incluyen el código completo del script, setTimeout, setInterval, Ajax, eventos del DOM
  2. Se disparan después del renderizado del DOM (los navegadores, para que las tareas macro internas de JavaScript y las tareas del DOM se ejecuten de forma ordenada, vuelven a renderizar la página después de completar una tarea macro y antes de comenzar la siguiente)

Tareas micro:

  1. Las tareas micro son definidas por la sintaxis ES6: incluyen Promesas, async/await
  2. Se disparan antes del renderizado del DOM (las tareas micro se ejecutan inmediatamente después de que la tarea actual termina, es decir, cuando el hilo principal queda vacío, se ejecutan todas las tareas micro primero, y luego se realiza el renderizado del DOM)

Después de ejecutar todo el código (una tarea macro), es decir, cuando la pila de ejecución del hilo principal está vacía, comienza el bucle de eventos (el Event Loop mencionado anteriormente). En cada ciclo, se verifica si hay tareas micro en la Cola de Eventos. Si las hay, se ejecutan todas antes de intentar el renderizado del DOM (si es necesario), y finalmente se ejecuta una tarea macro. Una vez completada una tarea macro, comienza el siguiente ciclo. En el siguiente ciclo, primero se ejecutan todas las tareas micro y luego una tarea macro, y así sucesivamente.

El flujo de ejecución de tareas macro y micro en JavaScript es el siguiente:

4. Tipos de tareas asíncronas y su momento de inserción en la cola de tareas

Las tareas se dividen en tareas macro (macro) y tareas micro (micro). El orden de ejecución en la cola de tareas es: las tareas micro tienen prioridad sobre las tareas macro

Tareas asíncronas micro:

  1. Promesas en ES6

Momento de inserción en la cola de tareas: cuando la operación asíncrona se completa con éxito (o falla), la función de devolución de llamada (o la función de error) se inserta en la cola de tareas

Tarea insertada en la cola: la función de devolución de éxito (o la función de error) se inserta en la cola de tareas

Ejemplo:

new Promise(function(resolver,rechazar){
   resolver();
}).then(function(){
   console.log('Tarea micro completada')
});

new Promise() se ejecuta inmediatamente, y la función de devolución de llamada function(){console.log('Tarea micro completada')} se registra en la Tabla de Eventos. Cuando se llama a resolved(), la función de devolución de llamada se coloca en la Cola de Eventos de micro-tareas, esperando para entrar en el hilo principal y ejecutarse.

Tareas asíncronas macro:

  1. Temporizadores (setTimeout y setInterval)

Momento de inserción en la Cola de Eventos: cuando llega el tiempo especificado

Tarea insertada en la Cola de Eventos: la función de devolución de llamada se inserta en la cola de eventos macro

Ejemplo:

temporizador(fn,5000)

fn entra en la Tabla de Eventos y comienza a contar el tiempo. Después de 5 segundos, la función de devolución de entrada entra en la Cola de Eventos. Cuando el código del hilo principal está vacío, el sistema lee la función de devolución de llamada de la Cola de Eventos y la ejecuta en el hilo principal.

  1. Solicitudes asíncronas Ajax

Momento de inserción en la Cola de Eventos: cuando la solicitud Ajax se completa

Tarea insertada en la Cola de Eventos: la función de devolución de llamada se inserta en la cola de eventos macro

Ejemplo:

solicitudAjax({
        url:'',
        datos:{},
        exito:function(){
            console.log('Solicitud exitosa')
        }
    })

La solicitud ajax entra en la Tabla de Eventos. Cuando la solicitud ajax se completa con éxito, la función de devolución de llamada exitosa entra en la Cola de Eventos. Cuando el código del hilo principal está vacío, el sistema lee la función de devolución de llamada exitosa de la Cola de Eventos y la ejecuta en el hilo principal.

  1. Eventos del DOM

Momento de inserción en la Cola de Eventos: cuando el usuario completa una acción de evento

Tarea insertada en la Cola de Eventos: la función de devolución de llamada se inserta en la cola de eventos macro

Ejemplo:

elemento.en('clic',fn,falso);

fn entra en la Tabla de Eventos. Cuando se activa el evento click, fn se coloca en la Cola de Eventos.

Resumen del mecanismo de ejecución de JavaScript:

1. El código síncrono se ejecuta línea por línea en la Pila de Llamadas (Call Stack)

2. Al encontrar código asíncrono (temporizadores, solicitudes de red, etc.), se pasa a las API del Navegador Web para su procesamiento, esperando el momento adecuado. Cuando llega el momento, se mueve a la Cola de Callbacks (Callback Queue)

3. Al encontrar código asíncrono de micro-tareas (Promise.then, async await, etc.), la función de devolución de callback se coloca en la cola de micro-tareas (micro task queue)

4. Si la Pila de Llamadas (Call Stack) está vacía (es decir, el código síncrono ha terminado de ejecutarse), se ejecutan todas las micro-tareas inmediatamente

5. Después de ejecutar las micro-tareas, se intenta el renderizado del DOM

6. Después del renderizado, se busca en la Cola de Eventos (Callback Queue). Si hay tareas esperando, se toma una tarea macro y se mueve a la Pila de Llamadas para su ejecución

7. Luego se continúa con los pasos 4 a 6 en un ciclo continuo, conocido como Bucle de Eventos

Analicemos el siguiente código para verificar si hemos comprendido el mecanismo de ejecución de JavaScript

Ejemplo 1:

    console.log('Inicio');
    temporizador(function(){
        console.log('Tarea asíncrona');
    } ,1000);
    new Promise(function(resolver){
        console.log('Promesa inmediata');
        resolver(); //Sin resolver, la función then no se ejecutará, para la prueba se ejecuta directamente
    }).then(function(){
        console.log('Micro-tarea');
    })
    console.log('Final');

Análisis:

* El script completo como primera tarea macro entra en el hilo principal, encuentra console.log y muestra "Inicio".

* Encuentra temporizador, que es una tarea asíncrona, se coloca en la tabla de eventos. Después de 1 segundo, su funnción de devolución de llamada se coloca en la Cola de Eventos de macro-tareas. La llamaremos temporizador1.

* Encuentra Promise. new Promise es una tarea síncrona, se ejecuta directamente mostrando "Promesa inmediata"; la función .then es una tarea asíncrona, se coloca en la tabla de eventos. Cuando se ejecuta resolve(), la función de devolución de then se coloca en la Cola de Eventos de micro-tareas. La llamaremos then1.

* Encuentra console.log('Final'), se muestra directamente "Final".

* Hasta aquí, las tareas del hilo principal han terminado, finalizando la primera ronda del bucle de eventos de macro-tareas. Comienza a leer los eventos en la Cola de Eventos, encontramos una micro-tarea then1, así que primero se lee then1 y se muestra "Micro-tarea". Hasta aquí termina formalmente la primera ronda del bucle de eventos.

* La segunda ronda del bucle de eventos comienza con temporizador1, mostrando "Tarea asíncrona". Finaliza la segunda ronda del bucle de eventos.

El resultado es: Inicio -> Promesa inmediata -> Final -> Micro-tarea -> Tarea asíncrona

Ejemplo 2:

console.log('1');
temporizador(function(){
    console.log('2');
    new Promise(function(resolver){
        console.log('4');
        resolver();
    }).then(function(){
        console.log('5');
    })
})

new Promise(function(resolver){
    console.log('7');
    resolver();
}).then(function(){
    console.log('8');
})

temporizador(function(){
    console.log('9');
    new Promise(function(resolver){
        console.log('11');
        resolver();
    }).then(function(){
        console.log('12');
    })
})
console.log('13');

Análisis:

  1. Flujo de la primera ronda del bucle de eventos:

* El script completo como primera tarea macro entra en el hilo principal, encuentra console.log('1'), se muestra directamente "1"

* Encuentra temporizador, su función de devolución de llamada se coloca en la Cola de Eventos de macro-tareas. La llamaremos temporizador1.

* Encuentra Promise. new Promise se ejecuta directamente, mostrando "7". La función de devolución de then se coloca en la Cola de Eventos de micro-tareas, la llamaremos then1.

* Encuentra otro temporizador, su función de devolución de llamada se coloca en la Cola de Eventos de macro-tareas. La llamaremos temporizador2.

* Encuentra console.log('13'), se muestra directamente "13"

* Hasta aquí termina la primera ronda del bucle de eventos de macro-tareas. En este momento, la situación en la Cola de Eventos es: Macro-tareas: temporizador1, temporizador2; Micro-tareas: then1

* Comienza a ejecutar todas las micro-tareas en la cola de eventos, encontramos solo then1, así que se ejecuta directamente, mostrando "8"

* Hasta aquí termina formalmente la primera ronda del bucle de eventos.

  1. Flujo de la segunda ronda del bucle de eventos:

* Comienza a ejecutar la macro-tarea temporizador1 desde la cola de eventos, mostrando directamente "2"

* Encuentra promise, new Promise se ejecuta directamente, mostrando "4". La función de devolución de then se coloca en la Cola de Eventos de micro-tareas, la llamaremos then2

* Hasta aquí termina la segunda ronda del bucle de eventos de macro-tareas. En este momento, la situación en la Cola de Eventos es: Macro-tareas: temporizador2; Micro-tareas: then2

* Comienza a ejecutar todas las micro-tareas en la cola de eventos, encontramos solo then2, se ejecuta directamente, mostrando "5"

* Hasta aquí termina formalmente la segunda ronda del bucle de eventos

  1. Flujo de la tercera ronda del bucle de eventos:

* Solo queda la macro-tarea temporizador2, se muestra directamente "9".

* Encuentra promise, new Promise se ejecuta directamente, mostrando "11". La función de devolución de then se coloca en la Cola de Eventos de micro-tareas, la llamaremos then3

* Termina la tercera ronda del bucle de eventos de macro-tareas, comienza a ejecutar la micro-tarea then3, mostrando "12"

* Termina la tercera ronda del bucle de eventos

  1. Todo el código realizó tres rondas de bucle de eventos, mostrando: 1 -> 7 -> 13 -> 8 -> 2 -> 4 -> 5 -> 9 -> 11 -> 12

Etiquetas: event-loop asincronia un-solo-hilo tareas-macro tareas-micro

Publicado el 6-3 02:02