Asincronismo en JavaScript

Este es el octavo tutorial de la serie 'Mis apuntes JavaScript'. En este tutorial aprenderemos sobre el asincronismo de JavaScript y que podemos hacer para obtener un resultado sincronico.

Índice

  1. Asincronismo
  2. Callbacks
  3. Promesas
  4. El método Fetch
  5. Async Await

1. Asincronismo

Resulta que JavaScript es un lenguaje de programación asincrono. Lo que quiere decir esto es que al ejecutar código JavaScript el hilo de ejecución continuará a pesar de encontrarse en situaciones en las que no obtenga un resultado inmediatamente. Por ejemplo, cuando hacemos el pedido de información a un servidor, la respuesta posiblemente demore un poco. Sin embargo, el hilo de ejecución de JavaScript continuará con las demás tareas que hay en el código.

Un ejemplo practico de esto seria una aplicación web que necesita llenar una tabla de datos, asi que el código hará un pedido al servidor de los datos que necesita llenar. Pero el hilo de ejecución no se detiene asi que ejecutará el código que pinta la tabla en el navegador. Esto se convierte en un problema ya que los datos del servidor llegan después de que la tabla se haya pintado en pantalla, una tabla sin tados obviamente.

Asi que trabajar con código asincrono puede tener muchas ventajas pero en casos como este presenta un gran problema. Pues bien, para solucionar esto algunas funciones de JavaScript tienen como parametro algo que se conoce como callback, que estudiaremos a continuación.

2. Callbacks

Un callback es simplemente una función que se pasa como parametro a otra funcion. Nosotros podemos construir funciones que usen callbacks de la siguiente manera.

function saludar(saludo) {
  saludo();
}

saludar(function (nombre) {
  console.log(`Hola ${nombre}`);
});

Una función en JavaScript que tiene un callback es el setTimeout(). Esta función ejecuta el callback despúes de esperar cierto tiempo el cual también le pasamos como parámetro.

// setTimeout(funcion, tiempo_ms);
setTimeout(function () {
  console.log("Hola");
}, 2000);

Esta función ejecuta el callback solo despúes de que hayan pasado 2000 milisegundos.

Con ayuda de esta función podemos crear un código que nos permita visualizar la asincronia de JavaScript.

// Usando función anonima
console.log("A");
setTimeout(function () {
  console.log("B");
}, 2000);
console.log("C");
/* Resultado del código anterior
 A
 C
 B  Despues de 2 segundo*/

La asincronia de JavaScript nos permite visualizar en consola la letra C mucho antes de la B, el cual demora 2 segundos. Como puedes ver la ejecución no se detiene dos segundos, esta continua y luego de dos segundos aparece C.

Para tener un cierto control en el código asincrono de JavaScript existen las promesas, las cuales estudiaremos a continuación.

3. Promesas

Las promesas llegan a JavaScript en la versión 6 de EcmaScript. Una promesa es el objeto que representa un valor que estará disponeble ahora, en el futuro o quiza nunca. Entonces como no sabemos cuando estarán disponibles. Por esta razón dejamos preparado dentro de la promesa el código que se ejecutará cuando el resultado llegue o incluso cuando el resultado es un error.

Podemos crear una promesa de la siguiente manera.

let x = 11;
const p = new Promise((resolve, reject) => {
  if (x == 10) {
    resolve("La variable es igual a 10");
  } else {
    reject("La variable no es igual a 10");
  }
});

En la función flecha del código anterior tenemos dos parametros, el primer parámetro resolve es una función que recibe como parametro el objeto que queremos que devuelva cuando el código tuvo el resultado que esperamos. Mientras que reject es una función que toma como parametro el objeto que devolverá si obtenemos un error, en nuestro código asincronico.

En resumen usando una promesa podemos recibir el resultado que necesita de una espera y ejecutar código luego de que el resultado llegue. Entonces probemos las promesas con el setTimeout.

let mensaje = new Promise((resolve, reject) => {
  setTimeout(function () {
    resolve("Este es el mensaje");
  }, 2000);
});

De esta forma creamos un objeto promesa con el mensaje como resultado resolve favorable de la promesa que devolverá luego de 2 segundos. Ahora para controlar la promesa usamos el método then y catch que vienen junto con las promesas.

mensaje
  .then((m) => {
    console.log(m);
  })
  .catch(function () {
    console.log("error");
  });

Entonces para capturar el resultado favorable de la promesa usamos then(). En nuestro caso se trata del mensaje que lo mostraremos usando un console.log(). Por otra parte el catch captura el resultado fallido o reject de la promesa. En nuestro caso no implementamos ninguno asi que nuestra función anonima dentro del catch no recibe ningun parametro y solo muestra el mensaje de error.

Listo ya sabemos que una promesa es como un tipo de dato y es por este motivo que muchas funciones de JavaScript y/o de librerias externas cuyo resultado es asincrono osea que demorará un tiempo en llegar están inplementados en promesas. Uno de estos métodos es fetch.

4. El método Fetch

Este método nos permite hacer una petición a un API y es justamente un callback. Por lo que tenemos que recibirlo usando then y catch de la siguiente forma.

let pokemones = fetch("https://pokeapi.co/api/v2/pokemon/1");

pokemones
  .then((res) => res.json())
  .then((data) => {
    console.log(data.name);
  })
  .catch((error) => console.log(error));

Esta es la forma que comunmente se utiliza a fetch pero hay que acomodarlo de tal manera que sea un poco más entendible.

let pokemones = fetch("https://pokeapi.co/api/v2/pokemon/1");

pokemones
  .then((res) => {
    return res.json();
  })
  .then((data) => {
    console.log(data.name);
  })
  .catch((error) => console.log(error));

Como puedes ver en el código anterior se pueden encadenar métodos then, pero se pueden hacer solo cuando el return del then anterior es una promesa. En este caso si nosotros vemos la implementación de json() veremos que este método devuelve una promesa. Por lo que el segundo then es la captura del resolve del json(). Asi que si intentamos descomponer más al código anterior tendríamos algo asi.

// primera promesa
let pokemones = fetch("https://pokeapi.co/api/v2/pokemon/1");

// segunda promesa
let respuesta = pokemones.then((res) => {
  return res.json();
});

respuesta.then((data) => {
  console.log(data.name);
});

Entonces si nosotros quisieramos ejecutar el fetch de manera ordenada y sincrona para ver los cinco primeros pokemones podemos hacer lo siguiente.

function obtener_pokemon(id) {
  let url = "https://pokeapi.co/api/v2/pokemon/" + id;
  return fetch(url).then((res) => {
    return res.json();
  });
}

obtener_pokemon(1)
  .then((data) => {
    console.log(data.name);
    return obtener_pokemon(2);
  })
  .then((data) => {
    console.log(data.name);
    return obtener_pokemon(3);
  })
  .then((data) => {
    console.log(data.name);
    return obtener_pokemon(4);
  })
  .then((data) => {
    console.log(data.name);
    return obtener_pokemon(5);
  })
  .then((data) => {
    console.log(data.name);
  });

5. Async Await

Ahora veremos una de las mejores caracteristicas que salieron en la especificación 8 de EcmaScript. El popular async await. Usamos el async para definir una función donde se encontrará el await que nos permitirá esperar una promesa de tal forma que podamos volver nuestro código sincrono.

function obtener_pokemon(id) {
  let url = "https://pokeapi.co/api/v2/pokemon/" + id;
  return fetch(url).then((res) => {
    return res.json();
  });
}

async function nombrar_pokemones() {
  let pokemon1 = await obtener_pokemon(1);
  console.log(pokemon1.name);
}

Usando el async await podemos lograr esto, que la variable espere por su resultado antes de ejecutar el console.log(). Y como podemos hacer que una variable espere podemos lograr incluso esto.

async function nombrar_pokemones() {
  for (let i = 1; i < 6; i++) {
    let pokemon = await obtener_pokemon(i);
    console.log(pokemon.name);
  }
}

Como ves, el async await nos permite esperar el resolve de una promesa de tal manera que podamos obtener un resultado sincrono.

O incluso podemos hacerlo usando forEach de la siguiente manera.

function obtener_pokemones() {
  let url = "https://pokeapi.co/api/v2/pokemon/";
  return fetch(url).then((res) => {
    return res.json();
  });
}

async function nombrar_pokemones() {
  let pokemones = await obtener_pokemones();
  pokemones.results.forEach((pokemon) => {
    console.log(pokemon.name);
  });
}

En este caso lo que pedimos al api es una lista con los 20 primeros pokemones pero estos están como una lista de objetos dentro del objeto result en la respuesta json que obtendremos. Asi que usamos el forEach para recorrer cada elemento del objeto results. Sin embargo, muchas veces lo que buscaremos será llevar una lista de objetos directamente a un arreglo para eso existe el método map. El cual lo podemos utilizar de la siguiente forma.

async function nombrar_pokemones() {
  let pokemones = await obtener_pokemones();
  let arregloPokemones = pokemones.results.map((pokemon) => pokemon.name);
  console.log(arregloPokemones);
}

El método map recibe una función flecha donde a partir del parametro que recibe que es cada elemento del results retorna lo que queremos que esté dentro del arreglo.

La forma de capturar un error cuando usamos Async Await es haciendo uso de try-catch como en el siguiente ejemplo.

try {
  const data = await fetch("https://pokeapi.co/api/v2/pokemon/" + name);
} catch (error) {
  console.log(error);
}