Hola Habr! Les presento la traducción del artículo
"Comprensión de JavaScript asincrónico" por Sukhjinder Arora.
Del autor de la traducción: Espero que la traducción de este artículo le ayude a familiarizarse con algo nuevo y útil. Si el artículo te ayudó, entonces no seas perezoso y agradece al autor del original. No pretendo ser un traductor profesional, recién estoy empezando a traducir artículos y me complacerá cualquier comentario significativo.JavaScript es un lenguaje de programación de un solo subproceso en el que solo se puede ejecutar una cosa a la vez. Es decir, en un solo hilo, el motor de JavaScript solo puede procesar 1 declaración a la vez.
Aunque los idiomas de subproceso único facilitan la escritura de código, ya que no tiene que preocuparse por problemas de concurrencia, también significa que no podrá realizar operaciones largas como acceder a la red sin bloquear el hilo principal.
Envíe una solicitud de API para algunos datos. Dependiendo de la situación, el servidor puede tardar un tiempo en procesar su solicitud, mientras que la ejecución de la transmisión principal se bloqueará debido a que su página web dejará de responder a las solicitudes.
Aquí es donde entra en juego la asincronía de JavaScript. Utilizando la asincronía de JavaScript (devoluciones de llamada, promesas y asíncrono / espera) puede realizar largas solicitudes de red sin bloquear el hilo principal.
Aunque no es necesario aprender todos estos conceptos para ser un buen desarrollador de JavaScript, es útil conocerlos.
Entonces, sin más preámbulos, comencemos.
¿Cómo funciona JavaScript síncrono?
Antes de profundizar en el trabajo de JavaScript asíncrono, primero comprendamos cómo se ejecuta el código síncrono dentro del motor de JavaScript. Por ejemplo:
const second = () => { console.log('Hello there!'); } const first = () => { console.log('Hi there!'); second(); console.log('The End'); } first();
Para comprender cómo se ejecuta el código anterior dentro del motor de JavaScript, necesitamos comprender el concepto del contexto de ejecución y la pila de llamadas (también conocida como la pila de ejecución).
Contexto de ejecución
El contexto de ejecución es un concepto abstracto del entorno en el que se evalúa y ejecuta el código. Cada vez que un código se ejecuta en JavaScript, se ejecuta en el contexto de ejecución.
El código de función se ejecuta dentro del contexto de la ejecución de la función, y el código global, a su vez, se ejecuta dentro del contexto de ejecución global. Cada función tiene su propio contexto de ejecución.
Pila de llamadas
Una pila de llamadas es una pila con una estructura LIFO (última entrada, primera salida, primera vez utilizada), que se utiliza para almacenar todos los contextos de ejecución creados durante la ejecución del código.
JavaScript tiene solo una pila de llamadas, ya que es un lenguaje de programación de subproceso único. La estructura LIFO significa que los elementos solo se pueden agregar y quitar de la parte superior de la pila.
Volvamos ahora al fragmento de código anterior e intentemos comprender cómo lo realiza el motor de JavaScript.
const second = () => { console.log('Hello there!'); } const first = () => { console.log('Hi there!'); second(); console.log('The End'); } first();

Entonces, ¿qué pasó aquí?
Cuando el código comenzó a ejecutarse, se creó un contexto de ejecución global (representado como
main () ) y se agregó a la parte superior de la pila de llamadas. Cuando se encuentra la llamada a la
primera función
() , también se agrega a la parte superior de la pila.
A continuación,
console.log ('¡Hola!') Se coloca en la parte superior de la pila de llamadas, después de la ejecución se elimina de la pila. Después de eso, llamamos a la
segunda función
() , por lo que se coloca en la parte superior de la pila.
console.log ('¡Hola!') se agrega a la parte superior de la pila y se elimina al finalizar la ejecución. La
segunda función
() se completa, también se elimina de la pila.
console.log ('The End') se agregó a la parte superior de la pila y se eliminó al final. Después de eso, la
primera función
() termina y también se elimina de la pila.
La ejecución del programa finaliza, por lo que el contexto de llamada global (
main () ) se elimina de la pila.
¿Cómo funciona el JavaScript asíncrono?
Ahora que tenemos una comprensión básica de la pila de llamadas y cómo funciona JavaScript síncrono, volvamos a JavaScript asíncrono.
¿Qué es el bloqueo?
Supongamos que estamos procesando el procesamiento de imágenes o la solicitud de red sincrónicamente. Por ejemplo:
const processImage = (image) => { console.log('Image processed'); } const networkRequest = (url) => { return someData; } const greeting = () => { console.log('Hello World'); } processImage(logo.jpg); networkRequest('www.somerandomurl.com'); greeting();
El procesamiento de imágenes y la solicitud de red lleva tiempo. Cuando se llama a la función
processImage () , su ejecución llevará algún tiempo, dependiendo del tamaño de la imagen.
Cuando se completa la función
processImage () , se elimina de la pila. Después de eso, se llama a la función
networkRequest () y se agrega a la pila. Esto nuevamente tomará algún tiempo antes de completar la ejecución.
Al final, cuando se ejecuta la función
networkRequest () , se llama a la función
greeting () , ya que contiene solo el método
console.log , y este método suele ser rápido, la función
greeting () se ejecutará y finalizará instantáneamente.
Como puede ver, debemos esperar a que se
complete la función (como
processImage () o
networkRequest () ). Esto significa que tales funciones bloquean la pila de llamadas o el hilo principal. Como resultado, no podemos realizar otras operaciones hasta que se ejecute el código anterior.
Entonces, ¿cuál es la solución?
La solución más simple son las funciones de devolución de llamada asincrónicas. Los usamos para que nuestro código no se bloquee. Por ejemplo:
const networkRequest = () => { setTimeout(() => { console.log('Async Code'); }, 2000); }; console.log('Hello World'); networkRequest();
Aquí utilicé el método
setTimeout para simular una solicitud de red. Recuerde que
setTimeout no
es parte del motor de JavaScript, es parte de la denominada API web (en el navegador) y API C / C ++ (en node.js).
Para comprender cómo se ejecuta este código, debemos tratar con algunos conceptos más, como el bucle de eventos y la cola de devolución de llamada (también conocida como la cola de tareas o la cola de mensajes).

El bucle de eventos, la API web y la cola de mensajes / tareas no son parte del motor de JavaScript; son parte del tiempo de ejecución de JavaScript de JavaScript o del tiempo de ejecución de JavaScript en Nodejs (en el caso de Nodejs). En Nodejs, las API web se reemplazan por API C / C ++.
Ahora, volvamos al código anterior y veamos qué sucede en el caso de ejecución asincrónica.
const networkRequest = () => { setTimeout(() => { console.log('Async Code'); }, 2000); }; console.log('Hello World'); networkRequest(); console.log('The End');

Cuando el código anterior se carga en el navegador,
console.log ('Hello World') se agrega a la pila y se elimina de ella al finalizar la ejecución. A continuación, se encuentra una llamada a la función
networkRequest () ; se agrega a la parte superior de la pila.
A continuación, se llama a la función
setTimeout () y se coloca en la parte superior de la pila. La función
setTimeout () tiene 2 argumentos: 1) una función de devolución de llamada y 2) tiempo en milisegundos.
setTimeout () inicia un temporizador durante 2 segundos en un entorno de API web. En este punto,
setTimeout () se completa y se elimina de la pila. Después de eso,
console.log ('The End') se agrega a la pila, se ejecuta y se elimina al finalizar.
Mientras tanto, el temporizador ha expirado, ahora la devolución de llamada se agrega a la cola de mensajes. Pero la devolución de llamada no se puede ejecutar de inmediato, y es aquí donde el ciclo de procesamiento de eventos ingresa al proceso.
Bucle de eventos
La tarea del bucle de eventos es realizar un seguimiento de la pila de llamadas y determinar si está vacía o no. Si la pila de llamadas está vacía, el bucle de eventos busca en la cola de mensajes para ver si hay devoluciones de llamada que están esperando para completarse.
En nuestro caso, la cola de mensajes contiene una devolución de llamada y la pila de ejecución está vacía. Por lo tanto, el bucle de eventos agrega una devolución de llamada a la parte superior de la pila.
Después de
agregar console.log ('Código asíncrono') a la parte superior de la pila, ejecutarlo y eliminarlo. En este punto, la devolución de llamada se completa y se elimina de la pila, y el programa se completa por completo.
Eventos DOM
La cola de mensajes también contiene devoluciones de llamada de eventos DOM, como clics y eventos de teclado. Por ejemplo:
document.querySelector('.btn').addEventListener('click',(event) => { console.log('Button Clicked'); });
En el caso de eventos DOM, el controlador de eventos está rodeado por la API web, esperando un evento específico (en este caso, un clic), y cuando ocurre este evento, la función de devolución de llamada se coloca en la cola de mensajes, esperando su ejecución.
Aprendimos cómo se ejecutan las devoluciones de llamada asíncronas y los eventos DOM, que utilizan una cola de mensajes para almacenar las devoluciones de llamadas que esperan ser ejecutadas.
Cola ES6 MicroTask
Nota autor de la traducción: en el artículo, el autor utilizó la cola de mensajes / tareas y la cola de tareas / micro-tareas, pero si traduce la cola de tareas y la cola de tareas, en teoría resulta lo mismo. Hablé con el autor de la traducción y decidí simplemente omitir el concepto de cola de trabajo. Si tienes alguna idea sobre esto, entonces te estoy esperando en los comentarios.
Enlace a la traducción del artículo por promesas del mismo autor
ES6 introdujo el concepto de cola de microtask, que Promises utiliza en JavaScript. La diferencia entre la cola de mensajes y la cola de microtask es que la cola de microtask tiene una prioridad más alta que la cola de mensajes, lo que significa que las "promesas" dentro de la cola de microtask se ejecutarán antes que las devoluciones de llamada en la cola de mensajes.
Por ejemplo:
console.log('Script start'); setTimeout(() => { console.log('setTimeout'); }, 0); new Promise((resolve, reject) => { resolve('Promise resolved'); }).then(res => console.log(res)) .catch(err => console.log(err)); console.log('Script End');
Conclusión
Script start Script End Promise resolved setTimeout
Como puede ver, la "promesa" se ejecutó antes de
setTimeout , todo esto porque la respuesta de la "promesa" se almacena dentro de la cola de microstareas, que tiene una prioridad más alta que la cola de mensajes.
Veamos el siguiente ejemplo, esta vez 2 "promesas" y 2
setTimeout :
console.log('Script start'); setTimeout(() => { console.log('setTimeout 1'); }, 0); setTimeout(() => { console.log('setTimeout 2'); }, 0); new Promise((resolve, reject) => { resolve('Promise 1 resolved'); }).then(res => console.log(res)) .catch(err => console.log(err)); new Promise((resolve, reject) => { resolve('Promise 2 resolved'); }).then(res => console.log(res)) .catch(err => console.log(err)); console.log('Script End');
Conclusión
Script start Script End Promise 1 resolved Promise 2 resolved setTimeout 1 setTimeout 2
Y nuevamente, nuestras dos "promesas" se ejecutaron antes de las devoluciones de llamada dentro de
setTimeout , ya que el ciclo de procesamiento de eventos considera que las tareas de la cola de microtask son más importantes que las tareas de la cola de mensajes / cola de tareas.
Si durante la ejecución de tareas aparece otra "Promesa" de la cola de microtask, se agregará al final de esta cola y se ejecutará antes de las devoluciones de llamada de la cola de mensajes, y no importa cuánto tiempo esperen su ejecución.
Por ejemplo:
console.log('Script start'); setTimeout(() => { console.log('setTimeout'); }, 0); new Promise((resolve, reject) => { resolve('Promise 1 resolved'); }).then(res => console.log(res)); new Promise((resolve, reject) => { resolve('Promise 2 resolved'); }).then(res => { console.log(res); return new Promise((resolve, reject) => { resolve('Promise 3 resolved'); }) }).then(res => console.log(res)); console.log('Script End');
Conclusión
Script start Script End Promise 1 resolved Promise 2 resolved Promise 3 resolved setTimeout
Por lo tanto, todas las tareas de la cola de microtask se completarán antes que las tareas de la cola de mensajes. Es decir, el bucle de procesamiento de eventos primero borrará la cola de microtask, y solo entonces comenzará a ejecutar devoluciones de llamada desde la cola de mensajes.
Conclusión
Entonces, aprendimos cómo funciona JavaScript asíncrono y los conceptos: pila de llamadas, bucle de eventos, cola de mensajes / cola de tareas y cola de microtask que conforman el tiempo de ejecución de JavaScript