¿Cómo funcionan las Promesas en Javascript Internamente? Introducción al Event Loop

Adéntrate en el universo de las promesas en JavaScript y descubre el poderoso funcionamiento que hay detrás. Explora cómo estas estructuras manejan de manera eficiente tareas asíncronas, revelando un mundo de posibilidades en el desarrollo web moderno.

En este artículo aprenderás:

  1. ¿Qué es una promesa en Javascript?
  2. ¿Cómo funciona una promesa internamente?
  3. Ejemplo 1 - setTimeout
  4. Ejemplo 2 - Promesa con método 'then'
  5. Ejemplo 3 - Promesa con async/await
  6. Resumen

¿Qué es una promesa en Javascript?

Las promesas en JavaScript son una construcción fundamental para manejar tareas asíncronas. Representan un valor que puede no estar disponible de inmediato, pero que se espera que lo esté en el futuro. En lugar de bloquear la ejecución del código hasta que se complete una tarea, las promesas permiten que el programa continúe su flujo normal de trabajo. Esto es especialmente útil cuando se trata de operaciones que pueden llevar tiempo, como solicitudes de red o acceso a bases de datos.

¿Cómo se crea una promesa?

Una promesa se crea con el constructor Promise. Este constructor toma una función como argumento, que a su vez toma dos funciones como argumentos:

  • resolve: Se llama cuando la promesa se resuelve satisfactoriamente.
  • reject: Se llama cuando la promesa falla.
const promesa = new Promise((resolve, reject) => {
  // ...
});

¿Cómo se usa una promesa?

Una promesa tiene tres estados posibles:

  • pending: El estado inicial de una promesa. Significa que la tarea asociada aún no se ha completado.
  • fulfilled: El estado de una promesa cuando se ha completado con éxito.
  • rejected: El estado de una promesa cuando ocurre un error durante la ejecución de la tarea.

¿Cómo se accede al valor de una promesa?

Hay dos formas de acceder al valor de una promesa:

  1. Usando el método then de la promesa.
  • then: Se ejecuta cuando la promesa se resuelve satisfactoriamente.
  • catch: Se ejecuta cuando la promesa falla.
  • finally: Se ejecuta siempre, tanto si la promesa se resuelve satisfactoriamente como si falla.
promesa
  .then((valor) => {
    // se ejecuta cuando la promesa se resuelve satisfactoriamente
  })
  .catch((error) => {
    // se ejecuta cuando la promesa falla
  })
  .finally(() => {
    // se ejecuta siempre, tanto si la promesa se resuelve satisfactoriamente como si falla
  });
  1. Usando el método await de la promesa. Para poder usar await, la función que lo usa debe ser async.
  • await: Espera a que la promesa se resuelva satisfactoriamente. Si la promesa falla, se lanza una excepción.
try {
  const valor = await promesa;
  // se ejecuta cuando la promesa se resuelve satisfactoriamente
} catch (error) {
  // se ejecuta cuando la promesa falla
} finally {
  // se ejecuta siempre, tanto si la promesa se resuelve satisfactoriamente como si falla
}

¿Cómo funciona una promesa internamente?

Pero... Javascript es un lenguaje de un solo hilo, ¿cómo puede manejar tareas asíncronas?

Para entender cómo funciona esto, es necesario entender cómo funciona el Event Loop (bucle de eventos) de Javascript.

¿Qué es el Event Loop?

El event loop es un componente fundamental en el entorno de ejecución de JavaScript que permite manejar tareas asíncronas de manera eficiente. De manera general, el event loop supervisa constantemente el estado del programa y garantiza que las tareas se ejecuten en el orden adecuado.

Componentes del Event Loop

El event loop se compone de tres componentes (principalmente):

  1. Call Stack (pila de llamadas)

Cuando se ejecuta un programa en JavaScript, se crea un entorno de ejecución que consta de un call stack. El call stack es una pila de ejecución que almacena las funciones que se están ejecutando actualmente. Cuando se llama a una función, se agrega al call stack. Cuando la función termina de ejecutarse, se elimina del call stack.

  1. APIs internas (APIs del navegador/Node.js)

Las APIs internas son funciones que proporcionan funcionalidad adicional al lenguaje JavaScript. Estas APIs se ejecutan en segundo plano y se comunican con el event loop a través de las task queues. En el caso de Node.js, las APIs internas son proporcionadas por el propio entorno de ejecución. En el caso de los navegadores, las APIs internas son proporcionadas por el navegador.

Estas APIs son la manera en la que Javascript puede ser un lenguaje concurrente, aunque sea de un solo hilo. Las tareas delegadas aquí no se ejecutan en el hilo principal (Main Thread)

Estas APIs incluyen:

  1. Task Queue (cola de tareas) (existen varias)

Las task queues son colas que almacenan las tareas asíncronas, como eventos de usuario, solicitudes de red o temporizadores. Estas tareas se añaden al callstack cuando este está vacío (o mejor dicho, cuando todas las tareas síncronas ya terminaron de ejecutar).

¿Cómo funciona el Event Loop?

El event loop funciona de la siguiente manera:

  1. Cuando se ejecuta un programa en JavaScript, se crea un entorno de ejecución que consta de un call stack.
  2. Cuando se llama a una función, se agrega al call stack.
  3. Cuando la función termina de ejecutarse, se elimina del call stack.
  4. Cuando se llama a una función asíncrona (una llamada a un servidor http, un temporizador, etc.), se delega la tarea a los APIs internos (ya sean del motor V8 en caso de los navegadores o Node en caso de los servidores).
  5. Cuando la tarea se completa, el API agrega el callback que venía aunado a la tarea asíncronas a la task queue correspondiente.
  6. Cuando el call stack está vacío, el event loop agrega el callback de la task queue al call stack.

Ejemplo 1 - setTimout

console.log("1");
setTimeout(() => console.log("2"), 5000);
console.log("3");
  1. Se agrega la función console.log("1") al call stack.
  2. Se ejecuta la función console.log("1") y se elimina del call stack.
  3. Se delega la tarea setTimeout(() => console.log("2"), 5) a los APIs internos.
  4. El API interno comienza un temporizador de 5 segundos.
  5. Se agrega la función console.log("3") al call stack.
  6. Se ejecuta la función console.log("3") y se elimina del call stack.
  7. Cuando el temporizador de 5 segundos termina, el API interno agrega la función console.log("2") a la task queue correspondiente.
  8. Cuando el call stack está vacío, el event loop agrega la función console.log("2") al call stack.
  9. Se ejecuta la función console.log("2") y se elimina del call stack.

Resultado

1
3
2

Nota: Sucedería lo mismo si el temporizador fuera de 0 segundos. La razón es porque a Javascript no le interesa el tiempo que se tarda en ejecutar una tarea asíncrona, sino el orden en el que se ejecutan las tareas. Todas las tareas síncronas se ejecutan antes que las asíncronas (al menos que se use el método await).

Ejemplo 2 - Promesa con método then

import api from "./api"; // api es una función que hace una llamada a un servidor http
console.log("1");
api()
  .then((data) => console.log(data))
  .catch((err) => console.error(err));
console.log("3");
  1. Se agrega la función console.log("1") al call stack.
  2. Se ejecuta la función console.log("1") y se elimina del call stack.
  3. Se delega la tarea api().then((data) => console.log(data)) a los APIs internos (la llamada por detrás estaría usando 'fetch', que es uno de los APIs en V8 y Node).
  4. El API interno hace la llamada a la API externa.
  5. Se agrega la función console.log("3") al call stack.
  6. Se ejecuta la función console.log("3") y se elimina del call stack.
  7. Cuando la llamada a la API externa termina, el API interno agrega la función console.log(data) a la task queue correspondiente (en caso de que la llamada sea exitosa, de lo contrario agregaría la función console.error(err)).
  8. Cuando el call stack está vacío, el event loop agrega la función console.log(data) al call stack.
  9. Se ejecuta la función console.log(data) y se elimina del call stack.

Resultado

1
3
data

Ejemplo 3 - Promesa con async/await

import api from "./api"; // api es una función que hace una llamada a un servidor http

async function run() {
  console.log("1");
  try {
    const data = await api();
    console.log(data);
  } catch (err) {
    console.error(err);
  }
  console.log("3");
}
run();
  1. Se agrega la función console.log("1") al call stack.
  2. Se ejecuta la función console.log("1") y se elimina del call stack.
  3. Se delega la tarea api() a los APIs internos (la llamada por detrás estaría usando 'fetch', que es uno de los APIs en V8 y Node).
  4. El API interno hace la llamada a la API externa.
  5. Como estamos usando async/await, esperamos a que se resuelva o rechace la promesa antes de continuar con la ejecución del programa.
  6. Cuando la llamada a la API externa termina, el API interno agrega la función console.log(data) a la task queue correspondiente (en caso de que la llamada sea exitosa, de lo contrario agregaría la función console.error(err)).
  7. Cuando el call stack está vacío, el event loop agrega la función console.log(data) al call stack.
  8. Se ejecuta la función console.log(data) y se elimina del call stack.
  9. Se agrega la función console.log("3") al call stack.
  10. Se ejecuta la función console.log("3") y se elimina del call stack.

Resultado

1
data
3

Resumen

  • Las promesas en JavaScript son una construcción fundamental para manejar tareas asíncronas.
  • Una promesa se crea con el constructor Promise.
  • Una promesa tiene tres estados posibles: pending, fulfilled y rejected.
  • Una promesa se resuelve satisfactoriamente con el método resolve y falla con el método reject.
  • Una promesa se usa con el método then y catch o con el método await.
  • El event loop es un componente fundamental en el entorno de ejecución de JavaScript que permite manejar tareas asíncronas de manera eficiente.
  • El event loop se compone de tres componentes: call stack, APIs internas y task queues.
  • El event loop funciona de la siguiente manera:
    1. Cuando se ejecuta un programa en JavaScript, se crea un entorno de ejecución que consta de un call stack.
    2. Cuando se llama a una función, se agrega al call stack.
    3. Cuando la función termina de ejecutarse, se elimina del call stack.
    4. Cuando se llama a una función asíncrona (una llamada a un servidor http, un temporizador, etc.), se delega la tarea a los APIs internos (ya sean del motor V8 en caso de los navegadores o Node en caso de los servidores).
    5. Cuando la tarea se completa, el API agrega el callback que venía aunado a la tarea asíncronas a la task queue correspondiente.
    6. Cuando el call stack está vacío, el event loop agrega el callback de la task queue al call stack.
  • Las APIs internas son la manera en la que Javascript puede ser un lenguaje concurrente, aunque sea de un solo hilo. Las tareas delegadas aquí NO se ejecutan en el hilo principal (Main Thread).

Ayúdame a mejorar este artículo

¿Quisieras complementar este artículo o encontraste algún error?¡Excelente! Envíame un correo.

  • seb@sebastianfdz.com