Artículo original: Synchronous vs Asynchronous JavaScript – Call Stack, Promises, and More por TAPAS ADHIKARY

Traducido por: Cristina Padilla

Permíteme comenzar este artículo preguntando "¿Qué es JavaScript?" Bueno, aquí está la respuesta más confusa pero precisa que he encontrado hasta ahora:

JavaScript es un lenguaje de programación de un solo procesamiento secuencial, no bloqueador, asíncrono, simultáneo y con mucha flexibilidad.

Espera un momento - ¿dije de un solo procesamiento secuencial y asíncrono al mismo tiempo? Si comprendes lo que significa de un solo procesamiento secuencial, probablemente lo asociarás a operaciones síncronas. Entonces, ¿cómo puede ser JavaScript asíncrono?

En este artículo, aprenderemos todo sobre las partes síncronas y asíncronas de JavaScript. Normalmente, ambas se usan en la programación web a diario.

Si prefieres aprender con un vídeo, el contenido de este artículo está disponible también como vídeotutorial aquí: 🙂

En este artículo, aprenderás:

  • Por qué JavaScript es síncrono.
  • En qué circunstancias se dan las operaciones asíncronas cuando JavaScript es de un solo procesamiento secuencial.
  • Cómo los términos síncrono y asíncrono te ayudarán a comprender mejor las promesas en JavaScript.
  • Muchos y sencillos, pero muy potentes ejemplos que muestran estos conceptos en detalle.

Las funciones de JavaScript son ciudadanos de primera clase

En JavaScript puedes crear y modificar una función, usarla como argumento, devolverla desde otra función y asignarla a una variable. Todas estas habilidades nos permiten usar las funciones en cualquier parte para colocar una cantidad de código de manera lógica.                      

block-function
Líneas de código organizadas en funciones de manera lógica

Necesitamos decirle al motor de JavaScript que ejecute las funciones invocándolas. Sería algo como esto:

// Definir una función
function f1() {
    // Hacer algo
    // Hacer algo otra vez
    // Otra vez
    // Etcétera...
}

// Invocar a la función
f1();

‌‌Por defecto, cada línea de la función se ejecuta secuencialmente, una línea tras otra. Lo mismo ocurre cuando invocas múltiples funciones en tu código. Línea tras línea.

JavaScript síncrono - Cómo funciona la ejecución de un conjunto de funciones

¿Qué sucede cuando defines una función y luego la invocas? El motor de JavaScript mantiene una pila de ejecución de datos llamada function execution stack. El objetivo de esto es hacer un seguimiento de la ejecución de la función. Realiza lo siguiente:

  • Cuando el motor de JavaScript invoca la función, la añade a la pila de ejecución y la ejecución comienza.
  • Si la función que está siendo ejecutada llama a otra función, el motor añade esta segunda función a la pila y comienza a ejecutarla.
  • Una vez terminada la ejecución de la segunda función, el motor la saca de la pila.
  • Se vuelve a la ejecución de la primera función desde el punto donde se dejó la última vez.
  • Cuando la ejecución de la primera función haya finalizado, el motor la saca de la pila.
  • Se continúa de esta manera hasta que no queda nada en la pila.

También se conoce a la pila de ejecución de funciones como Call Stack.‌

stack
Pila de ejecución de funciones o Call Stack

Echémosle un vistazo a un ejemplo de tres funciones que se ejecutan una a una:

function f1() {
  // algo de código
}
function f2() {
  // algo de código
}
function f3() {
  // algo de código
}

// Invoca a las funciones una a una
f1();
f2();
f3();

Ahora veamos qué sucede con la pila de ejecución de funciones‌:‌

first-flow
Este ejemplo muestra paso a paso el orden de la ejecución

¿Has visto lo que sucede aquí? Primero, f1() va a la pila, se ejecuta y sale. Después f2() hace lo mismo y finalmente f3() también. Después de este proceso, la pila se encuentra vacía, sin nada más que ejecutar.

Ok, vamos a ver ahora un ejemplo más complejo. Aquí la función f3() invoca a la función f2() que a su vez invoca también a la función f1().              

function f1() {
  // Algo de código
}
function f2() {
  f1();
}
function f3() {
  f2();
}
f3();

‌Vamos a ver qué ocurre en la pila de ejecución de funciones:‌

second-flow
Este ejemplo muestra paso a paso el orden de la ejecución

Observa cómo f3() va a la pila en primer lugar invocando a la función f2(). Así pues, f2() también se añade a la pila mientras f3() aún está en ella. La función f2()invoca a f1(), la cual se añade a la pila junto con f2()y f3().

En primer lugar, f1() termina de ejecutarse y sale de la pila. Justo después f2() termina, y finalmente f3().

En resumen, todo lo que sucede dentro de la pila de ejecución de funciones es secuencial. Ésta es la parte síncrona de JavaScript. El procesamiento de JavaScript se asegura de que todo lo que está en la pila se ejecuta antes de comenzar con algo nuevo.

¡Estupendo! Ahora que comprendemos cómo funcionan las operaciones síncronas en JavaScript, podemos indagar en su lado asíncrono. ¿Estás preparad@?

JavaScript asíncrono - Cómo funcionan las APIs del Navegador y las Promesas

La palabra asincronía significa que no sucede al mismo tiempo. ¿Qué significa esto en el contexto de JavaScript?

Por lo general, la ejecución de cosas secuencialmente funciona bien. Sin embargo, a veces puede que necesites tomar datos de un servidor o retrasar la ejecución de una función, algo que no prevés que ocurra AHORA. Es decir, quieres que tu código se ejecute de manera asíncrona.

Bajo estas circunstancias, es posible que no quieras que el motor de JavaScript detenga la ejecución de otro código secuencial. Para ello, el motor de JavaScript necesita manejar las cosas un poco más eficientemente en este caso.

Podemos clasificar la mayoría de las operaciones asíncronas de JavaScript de 2 maneras:

  1. Eventos o funciones Browser API/Web API. Estos incluyen métodos como setTimeout, o controladores de eventos como clic, mouse over, scroll y muchos más.
  2. Promesas. Un objeto único en JavaScript que nos permite realizar operaciones asíncronas.

No te preocupes si las promesas son un concepto nuevo para ti. Por ahora no necesitas saber más para poder seguir este artículo. Al final del artículo, he añadido algunos enlaces para principiantes para que puedas aprender sobre las promesas.

Cómo manejar las APIs de un navegador/Web APIs

Las APIs del navegador como setTimeout y los controladores de eventos dependen de las funciones callback . Una función callback se ejecuta cuando una operación asíncrona se completa. A continuación puedes ver un ejemplo de cómo funciona la función setTimeout :

function printMe() {
  console.log('print me');
}

setTimeout(printMe, 2000);

La función setTimeout ejecuta una función después de que haya transcurrido un determinado tiempo. En el código anterior, el texto print me se muestra en la consola después de 2 segundos.

Imagínate que tenemos las siguientes líneas de código después de la función  setTimeout :

function printMe() {
  console.log('print me');
}

function test() {
  console.log('test');
}

setTimeout(printMe, 2000);
test();

¿Qué esperamos que ocurra aquí? ¿Cuál crees que será el resultado?

¿Esperará el motor de JavaScript esperará durante 2 segundos para invocar la función test() y el resultado será?:

printMe
test

¿O se las arreglará para mantener la función callback setTimeout a un lado y continuará ejecutando el resto? Así que el resultado podría ser el siguiente:

test
printMe

Si apostaste por la segunda opción, estás en lo cierto. Ahí es donde entra en juego el mecanismo asíncrono.

Cómo funciona el Callbak Queue en JavaScript (o la cola de tareas )

JavaScript mantiene una cola de funciones callback conocida como callback queue o task queue. La cola de una estructura de datos es First-In-First-Out(FIFO). Es decir, la función callback que se añada primero a la cola tiene la oportunidad de salir primero. Pero la pregunta es:

  • ¿Cuándo la pone el motor de JavaScript en la cola?
  • ¿Cuándo la saca el motor de JavaScript de la cola?
  • ¿A dónde va cuando sale de la cola?
  • Y lo que es más importante, ¿cómo relacionamos todo esto con la parte asíncrona de JavaScript?

¡Vaya, cuántas preguntas! Vamos a averiguar las repuestas con la ayuda de la siguiente imagen:

taskQ

La imagen superior muestra el call stack que ya hemos visto. Existen dos secciones más que rastrear si la API de un navegador (como setTimeout) se activa y pone la función callback de la API a la cola.

El motor de JavaScript sigue ejecutando las funciones en el call stack. Como no pone la función callback directamente en la pila, no hay código alguno esperando ni bloqueando la ejecución en la pila.

El motor crea un bucle que busca en la cola regularmente para encontrar lo que necesita extraer. Extrae una función callback de la cola a la call stack cuando la pila está vacía. Es entonces cuando la función callback se ejecuta como cualquier otra función en el stack. El bucle continúa. Este bucle se conoce como Event Loop.

Así que la moraleja de la historia es la siguiente:

  • Si la API de un navegador se activa, las funciones callback se quedan en la cola.
  • El código se sigue ejecutando de manera normal en la pila.
  • El event loop comprueba si hay una función callback en la cola.
  • Si es así, pasa la función callback de la cola a la pila y la ejecuta.
  • El bucle continúa.

Ahora vamos a ver cómo funciona el siguiente código:

function f1() {
    console.log('f1');
}

function f2() {
    console.log('f2');
}

function main() {
    console.log('main');
    
    setTimeout(f1, 0);
    
    f2();
}

main();

El código ejecuta una función setTimeout con una función callback f1(). Ten en cuenta que le hemos asignado un valor de 0 como retraso. Esto significa que esperamos que la función f1() se ejecute inmediatamente. Justo después de setTimeout, ejecutamos la función f2().

¿Cuál crees que será el resultado? Aquí lo tienes:

main
f2
f1

Podrías estar pensando que f1 debería imprimirse antes que f2, ya que no hemos retrasado la ejecución de f1. Sin embargo, ese no es el caso. ¿Recuerdas el mecanismo del event loop mencionado anteriormente? Vamos a ver el código anterior paso a paso:

third-flow
Event loop - observa la ejecución paso a paso

Aquí tienes los pasos descritos:

  1. La función main() entra en el call stack.
  2. Tiene una función console log para imprimir la palabra main. Se ejecuta el console.log('main') y sale del stack.
  3. Se lleva a cabo la API del navegador setTimeout.
  4. La función callback entra en el callback queue.
  5. La ejecución se produce de manera normal en el stack, así que f2() pasa al stack. Se ejecuta el console log de la función f2()y ambos salen del stack.
  6. La función main() también sale del stack.
  7. El event loop identifica que el call stack está vacío y que hay una función callback en la cola.
  8. La función callback f1() pasa al stack. Comienza la ejecución. El console log se ejecuta y f1() también sale del stack.
  9. En este punto, no hay nada más que ejecutar ni en el stack ni en la cola.

Espero que ahora comprendas cómo funciona internamente la parte asíncrona de JavaScript. Pero eso no es todo. Aún nos queda echarle un vistazo a las promesas.

Cómo maneja el motor de JavaScript las promesas

En JavaScript las promesas son objetos especiales que ayudan a realizar operaciones asíncronas.

Puedes crear una promesa usando el constructor Promise. Necesitas pasarle una función llamada executor. En esta función defines lo que quieres hacer cuando una promesa devuelva algo con éxito o ,por el contrario, dé error. Puedes hacer esto llamando los métodosresolve y reject.

Aquí tienes un ejemplo de una promesa en JavaScript:

const promise = new Promise((resolve, reject) =>
        resolve('I am a resolved promise');
);

Después de que la promesa se ejecute, podemos manejar el resultado con el método .then() y el error con el método .catch().

promise.then(result => console.log(result))

Las promesas se usan cada vez que utilices el método fetch() para tomar datos de una tienda.

El punto aquí es que el motor de JavaScript no usa el mismo callback queue que vimos anteriormente con las APIs del navegador. Usa otra cola especial llamada cola de trabajo o Job Queue.

¿Qué es la Job Queue en JavaScript?

Cada vez que una promesa aparece en nuestro código, la función ejecutora entra en la cola de trabajo. El event loop funciona, como siempre, y le da prioridad a los elementos que están en job queue o cola de trabajo por encima de los elementos que se encuentran en la callback queue cuando el stack está libre.

El elemento en la callback queue se llama macro task, mientras que el elemento en la job queue se llama micro task.

Así que todo el proceso funciona así:

  • Con cada bucle del event loop, se completa una tarea de la callback queue.
  • Una vez que una tarea se haya completado, el event loop visita la job queue. Completa todas las micro tasks en la cola de trabajo antes de pasar a otra cosa.
  • Si ambas colas tienen algo esperando, la job queue tiene prioridad sobre la callback queue.

La imagen inferior muestra la inclusión de la job queue o cola de trabajo junto con otros elementos existentes.

JObQ

Ahora, veamos un ejemplo para entender mejor esta secuencia:

function f1() {
    console.log('f1');
}

function f2() {
    console.log('f2');
}

function main() {
    console.log('main');
    
    setTimeout(f1, 0);
    
    new Promise((resolve, reject) =>
        resolve('I am a promise')
    ).then(resolve => console.log(resolve))
    
    f2();
}

main();

En el código superior, tenemos una función setTimeout() como antes pero hemos introducido una promesa justo después. Recuerda todo lo que hemos aprendido hasta ahora y adivina el resultado.

Si tu respuesta coincide con esta, estás en lo cierto:

main
f2
I am a promise
f1

Ahora veamos el proceso de las acciones:

fourth-flow
Callback queue vs. Job queue

El flujo es casi el mismo que el anterior, pero es muy importante observar cómo los elementos de la cola de trabajo priorizan los elementos de la cola de tareas. Por otra parte, observa cómo no importa si el setTimeout tiene un retraso cero. Se trata de que la job queue que viene antes de el callback queue.

¡Estupendo!, hemos aprendido todo lo que necesitamos para comprender la ejecución síncrona y asíncrona en JavaScript.

¡Una prueba para ti!

Pongamos a prueba tus conocimientos con este test. Adivina el resultado del siguiente código aplicando el conocimiento que has adquirido hasta ahora:

function f1() {
 console.log('f1');
}

function f2() { 
    console.log('f2');
}

function f3() { 
    console.log('f3');
}

function main() {
  console.log('main');

  setTimeout(f1, 50);
  setTimeout(f3, 30);

  new Promise((resolve, reject) =>
    resolve('I am a Promise, right after f1 and f3! Really?')
  ).then(resolve => console.log(resolve));
    
  new Promise((resolve, reject) =>
    resolve('I am a Promise after Promise!')
  ).then(resolve => console.log(resolve));

  f2();
}

main();

El resultado esperado sería:

main
f2
I am a Promise, right after f1 and f3! Really?
I am a Promise after Promise!
f3
f1

¿Te gustaría realizar más pruebas como esta? Dirígete a este repositorio y practica con más ejercicios.

Si te bloqueas o necesitas alguna aclaración, no dudes en ponerte en contacto conmigo a través de Twitter.

En resumen

  • El motor de JavaScript usa la pila de estructura de datos para realizar un seguimiento de las actuales funciones ejecutadas. La pila es conocida como la function execution stack.
  • La function execution stack (es decir, la call stack) ejecuta las funciones de manera secuencial, línea a línea, una por una.
  • Las web APIs o APIs del navedagor usan las funciones callback para completar las tareas cuando una operación asíncrona se realiza. La función callback se coloca en la callback queue.
  • Las funciones del ejecutor de promesas se colocan en la job queue.
  • Con cada bucle del event loop una macro tarea se completa fuera de la callback queue.
  • Una vez que la tarea se complete, el event loop visita la job queue y completa todas las micro tareas dentro de la job queue antes de pasar a otra cosa.
  • Si ambas colas tienen algo esperando al mismo tiempo, la job queue tiene prioridad sobre la callback queue.

Antes de concluir...

Eso es todo por ahora. Espero que este artículo te haya parecido útil y te ayude a comprender mejor los conceptos de sincronía y asincronía en JavaScript.

¡Conectemos! Puedes seguirme en Twitter(@tapasadhikary), en mi canal de Youtube y en GitHub(atapas).