Iagovar

Javascript: Funciones asíncronas: Callbacks, Promesas, async/await

El abuso de las funciones anónimas en JS puede complicar su aprendizaje

Cuando empieces con las funciones asíncronas, es habitual que te las enseñen por el orden del título, es decir, primero los Callbacks, luego las promesas y luego async/await.

Este orden no responde a otra cosa que el orden de introducción de estas características en JavaScript. En mi opinión, el abuso de funciones anónimas (especialmente funciones flecha) puede llegar a complicar la lectura de los códigos de ejemplo.

Veamos si podemos simplificarlo.

¿Qué sentido tiene la asincronía?

Como sabrás, los intérpretes de lenguajes como Python o JavaScript leen los archivos de arriba a abajo. Ejecutarán la primera llamada que encuentren con ámbito (scope) de más bajo nivel, y llenarán la memoria de todos los objetos necesarios para llevar a cabo dicha ejecución.

Sin embargo, muchas de las tareas que involucran código requieren obtener o grabar datos en una base de datos, o interactuar con una API, o quizá algún archivo en una red. Todas estas tareas se pueden demorar por muchos factores, y muy probablemente no quieras detener la ejecución de tu programa hasta que se completen.

Observa el siguiente código.


function primeraFuncion() {
  console.log("Esto y en la primera funcion")

  // Vamos a hacer una petición a un servidor
  // https://lenguajejs.com/javascript/peticiones-http/xhr/

  const request = new XMLHttpRequest(); // Creamos una instancia
  const url = 'https://jsonplaceholder.typicode.com/todos/2';
  request.open("GET", url, false); // Abrimos una conexión, false -> Síncrono
  request.send(); // Enviamos la petición al servidor

  console.log(request.response)

  segundaFuncion()
}

function segundaFuncion() {
  console.log("Estoy en la segunda funcion")
}

primeraFuncion()

console.log("Hemos finalizado")

/* Produce este output en la consola:

    "Esto y en la primera funcion"
    "{
      \"userId\": 1,
      \"id\": 2,
      \"title\": \"quis ut nam facilis et officia qui\",
      \"completed\": false
    }"
    "Estoy en la segunda funcion"
    "Hemos finalizado"
*/

¿Pero qué pasa si la respuesta del servidor tarda? Nuestro programa se detendría hasta que se obtuviese la petición. Podemos hacer, sin embargo, que el código siga corriendo y que nuestra petición se imprima en pantalla simplemente cuando se haya completado.

Para ello pasamos true como argumento del método .open() y hacemos uso del evento .onload como se muestra a continuación.


function primeraFuncion() {
  console.log("Esto y en la primera funcion")

  // Vamos a hacer una petición a un servidor
  // https://lenguajejs.com/javascript/peticiones-http/xhr/

  const request = new XMLHttpRequest(); // Creamos una instancia
  const url = 'https://jsonplaceholder.typicode.com/todos/2';
  request.open("GET", url, true); // Abrimos una conexión, true -> Síncrono
  request.send(); // Enviamos la petición al servidor

  request.onload = function() { console.log(request.response)} // Evento que se lanza al finalizar la carga

  segundaFuncion()
}

function segundaFuncion() {
  console.log("Estoy en la segunda funcion")
}

primeraFuncion()

console.log("Hemos finalizado")

/* Produce este output en la consola:

    "Esto y en la primera funcion"
    "Estoy en la segunda funcion"
    "Hemos finalizado"
    "{
      \"userId\": 1,
      \"id\": 2,
      \"title\": \"quis ut nam facilis et officia qui\",
      \"completed\": false
    }"
*/

En la mayoría de tutoriales que vas a encontrar es posible que no veas el uso de XMLHttpRequest() sino de fetch(), que ya trae promesas integradas.

Callbacks, asincronía antes de Promesas y Async/Await

Los callbacks son simplemente funciones que se pasan como argumento de otra función. El ejemplo más sencillo que se me ocurre es el siguiente.


function primeraFuncion(unaVariableDeTexto, funcionDeCallBack) {
  // El código de la función.
  // Cuando termina nuestra función, llamamos a funcionDeCallBack()

  funcionDeCallBack(unaVariableDeTexto)
}

function imprimirTexto(textoParaImprimir) {
  console.log(textoParaImprimir)
}

primeraFuncion("Este es un texto de ejemplo", imprimirTexto)

// Muestra en consola: "Este es un texto de ejemplo"

Bien, ¿Qué está corriendo aquí exáctamente?

Como habrás observado, callbacks es lo que hemos usado en el ejemplo que abre este post.

Promesas para resolver la dificultad de los callbacks

Lo que vas a leer por ahí es que el callback hell fué la motivación detrás de crear premisas. Una forma de evitar muchos callbacks anidados que hace que el código crezca horizontalmente.

Sin embargo, no me parece muy convincente. Como explica este comentario de StackOverflow, el mismo problema se puede producir con promesas.

El problema de JavaScript es que la gente abusa mucho de funciones anónimas, y esto es lo que produce código que cuesta mucho leer.

Si estás buscando un bloque de código, sin abuso de funciones anónimas, que te explique el funcionamiento de promesas, aquí lo tienes:


let miPromesa = new Promise(funcionDePromesa);

function funcionDePromesa(seResuelve, daError) {
// Función que ejecuta el código de producción

  const unaVariable = 0;

  if (unaVariable == 1) {
    seResuelve("Bien");
  } else {
    daError("Mal");
  };

};

// Código que consume la promesa
miPromesa.then(
  function(value) { /* Si se resuelve bien */ imprimir(value)},
  function(error) { /* Si falla */ imprimir(error)}
);

function imprimir(argumento) {console.log(argumento)}

Te recomiendo probarlo en JS Bin.

Async/Await: Haciendo las promesas más fáciles de manejar

Básicamente async permite envolver el resultado de una función en una promesa, y await detiene la ejecución de una función hasta que la promesa se ha resuelto.


// Creamos la función asíncrona/ejecutora
async function generaTexto() {
  let unTextoCualquiera = "Devuelvo este texto"
  return unTextoCualquiera;
}

// Podemos consumir la promesa con .then()
// Devuelve en consola: Devuelvo este texto
generaTexto().then(
  (x) => (console.log(x)),
  (y) => (console.log(y))
)

// Podemos consunsumirla dentro de una función asíncrona con await
async function getTexto() {
  let unTexto = await generaTexto();
  console.log(unTexto);
}

// Llamamos a nuestra función de consumo para ejecutarla
// Devuelve en consola: Devuelvo este texto
getTexto()

// ¿Y qué pasa si tratamos de consumir generaTexto() directamente?
// Devuelve en consola: Promise { [[PromiseStatus]]:resolved, [[PromiseValue]]:Devuelvo este texto }
console.log(generaTexto())

¿Qué está sucediendo aquí?