Comprender la espera asincrónica | Programar Plus

Al escribir código para la web, eventualmente necesitará realizar algún proceso que puede tardar unos minutos en completarse. JavaScript realmente no puede realizar múltiples tareas, por lo que necesitaremos una forma de manejar esos procesos de ejecución prolongada.

Async/Await es una forma de manejar este tipo de secuenciación basada en el tiempo. Es especialmente bueno cuando necesita realizar algún tipo de solicitud de red y luego trabajar con los datos resultantes. ¡Vamos a profundizar en!

¿Promesa? Promesa.

Async/Await es un tipo de Promesa. Las promesas en JavaScript son objetos que pueden tener múltiples estados (algo así como los de la vida real ☺️). Las promesas hacen esto porque a veces lo que pedimos no está disponible de inmediato y necesitaremos poder detectar en qué estado se encuentra.

Considere que alguien le pide que prometa hacer algo por él, como ayudarlo a mudarse. No es el estado inicial, donde han pedido. Pero no has cumplido tu promesa hasta que apareces y los ayudas a mudarse. Si cancelas tus planes, rechazaste la promesa.

De manera similar, los tres estados posibles para una promesa en JavaScript son:

  • pendiente: cuando llama por primera vez a una promesa y no se sabe qué devolverá.
  • cumplida: lo que significa que la operación se completó con éxito
  • rechazado: la operación falló

He aquí un ejemplo de una promesa en estos estados:

Aquí está el estado cumplido. Almacenamos una promesa llamada getSomeTacos, pasando los parámetros de resolución y rechazo. Le decimos a la promesa que está resuelta, y eso nos permite luego consolar el registro dos veces más.

const getSomeTacos = new Promise((resolve, reject) => {
  console.log("Initial state: Excuse me can I have some tacos");

  resolve();
})
  .then(() => {
    console.log("Order some tacos");
  })
  .then(() => {
    console.log("Here are your tacos");
  })
  .catch(err => {
    console.error("Nope! No tacos for you.");
  });
> Initial state: Excuse me can I have some tacos
> Order some tacos
> Here are your tacos

ver la pluma
Estados de promesa por Sarah Drasner (@sdras)
en CodePen.

Si elegimos el estado rechazado, haremos la misma función pero esta vez lo rechazaremos. Ahora lo que se imprimirá en la consola es el estado inicial y el error de captura:

const getSomeTacos = new Promise((resolve, reject) => {
  console.log("Initial state: Excuse me can I have some tacos");

  reject();
})
  .then(() => {
    console.log("Order some tacos");
  })
  .then(() => {
    console.log("Here are your tacos");
  })
  .catch(err => {
    console.error("Nope! No tacos for you.");
  });
> Initial state: Excuse me can I have some tacos
> Nope! No tacos for you.

Y cuando seleccionemos el estado pendiente, simplemente console.log lo que almacenamos, getSomeTacos. ¡Esto imprimirá un estado pendiente porque ese es el estado en el que se encuentra la promesa cuando la registramos!

console.log(getSomeTacos)
> Initial state: Excuse me can I have some 🌮s
> Promise {<pending>}
> Order some &#x1f32e;s
> Here are your &#x1f32e;s

¿Entonces que?

Pero aquí hay una parte que me resultó confusa al principio. Para obtener un valor de una promesa, debe usar .then() o algo que devuelve la resolución de tu promesa. Esto tiene sentido si lo piensa, porque necesita capturar lo que eventualmente será, en lugar de lo que es inicialmente, porque inicialmente estará en ese estado pendiente. Por eso lo vimos impreso Promise {<pending>} cuando registramos la promesa anterior. Nada se había resuelto todavía en ese punto de la ejecución.

Async/Await es realmente azúcar sintáctico además de esas promesas que acabas de ver. Aquí hay un pequeño ejemplo de cómo podría usarlo junto con la promesa de programar múltiples ejecuciones.

async function tacos() {
  return await Promise.resolve("Now and then I get to eat delicious tacos!")
};

tacos().then(console.log)

O un ejemplo más detallado:

// this is the function we want to schedule. it's a promise.
const addOne = (x) => {
  return new Promise(resolve => {
    setTimeout(() => { 
      console.log(`I added one! Now it's ${x + 1}.`)
      resolve()
    }, 2000);
  })
}

// we will immediately log the first one, 
// then the addOne promise will run, taking 2 seconds
// then the final console.log will fire
async function addAsync() {
  console.log('I have 10')
  await addOne(10)
  console.log(`Now I'm done!`)
}

addAsync()
> I have 10
> I added one! Now it's 11.
> Now I'm done!

ver la pluma
Ejemplo asíncrono 1 por Sarah Drasner (@sdras)
en CodePen.

Una cosa (a) espera a otra

Un uso común de Async/Await es usarlo para encadenar varias llamadas asíncronas. Aquí, buscaremos algo de JSON que usaremos para pasar a nuestra próxima llamada de búsqueda para averiguar qué tipo de cosas queremos obtener de la segunda API. En nuestro caso, queremos acceder a algunos chistes de programación, pero primero debemos averiguar desde una API diferente qué tipo de cotización queremos.

El primer archivo JSON se ve así: queremos que el tipo de cotización sea aleatorio:

{
  "type": "random"
}

La segunda API devolverá algo parecido a esto, dado que random parámetro de consulta que acabamos de obtener:

{
  "_id":"5a933f6f8e7b510004cba4c2",
  "en":"For all its power, the computer is a harsh taskmaster. Its programs must be correct, and what we wish to say must be said accurately in every detail.",
  "author":"Alan Perlis",
  "id":"5a933f6f8e7b510004cba4c2"
}

llamamos al async luego déjelo esperar para ir a recuperar el primero .json antes de que obtenga datos de la API. Una vez que eso suceda, podemos hacer algo con esa respuesta, como agregarla a nuestra página.

async function getQuote() {
  // get the type of quote from one fetch call, everything else waits for this to finish
  let quoteTypeResponse = await fetch(`https://s3-us-west-2.amazonaws.com/s.cdpn.io/28963/quotes.json`)
  let quoteType = await quoteTypeResponse.json()

    // use what we got from the first call in the second call to an API, everything else waits for this to finish
  let quoteResponse = await fetch("https://programming-quotes-api.herokuapp.com/quotes/" + quoteType.type)
  let quote = await quoteResponse.json()

  // finish up
  console.log('done')
}

Incluso podemos simplificar esto usando literales de plantilla y funciones de flecha:

async function getQuote() {
  // get the type of quote from one fetch call, everything else waits for this to finish
  let quoteType = await fetch(`quotes.json`).then(res => res.json())

    // use what we got from the first call in the second call to an API, everything else waits for this to finish
  let quote = await fetch(`programming-quotes.com/${quoteType.type}`).then(res => res.json())

  // finish up
  console.log('done')
}

getQuote()

Aquí hay una explicación animada de este proceso.

ver la pluma
Descripción animada de Async Await por Sarah Drasner (@sdras)
en CodePen.

Prueba, captura, finalmente

Eventualmente querremos agregar estados de error a este proceso. tenemos a mano try, catch, y finally bloques para esto.

try {
  // I’ll try to execute some code for you
}
catch(error) {
  // I’ll handle any errors in that process
} 
finally {
  // I’ll fire either way
}

Reestructuremos el código anterior para usar esta sintaxis y detectar cualquier error.

async function getQuote() {
  try {
    // get the type of quote from one fetch call, everything else waits for this to finish
    let quoteType = await fetch(`quotes.json`).then(res => res.json())

      // use what we got from the first call in the second call to an API, everything else waits for this to finish
    let quote = await fetch(`programming-quotes.com/${quoteType.type}`).then(res => res.json())

    // finish up
    console.log('done')
  }

  catch(error) {
    console.warn(`We have an error here: ${error}`)
  }
}

getQuote()

no usamos finally aquí porque no siempre lo necesitamos. Es un bloque que siempre se activará tanto si tiene éxito como si falla. Considere usar finally cada vez que estás duplicando cosas en ambos try y catch. Usualmente uso esto para un poco de limpieza. Escribí un artículo sobre esto, si tienes curiosidad por saber más.

Eventualmente, es posible que desee un manejo de errores más sofisticado, como una forma de cancelar una función asíncrona. Desafortunadamente, no hay forma de hacer esto de forma nativa, pero afortunadamente, Kyle Simpson creó una biblioteca llamada CAF que puede ayudar.

Otras lecturas

Es común que las explicaciones de Async/Await comiencen con devoluciones de llamada, luego promesas y use esas explicaciones para enmarcar Async/Await. Dado que Async/Await cuenta con un buen soporte en estos días, no recorrimos todos estos pasos. Todavía es un fondo bastante bueno, especialmente si necesita mantener bases de código más antiguas. Estos son algunos de mis recursos favoritos:

  • JavaScript asíncrono: desde devoluciones de llamada hasta promesas y asíncrono/espera (Tyler McGinnis)
  • JavaScript asíncrono con async/await (Marius Schulz)
  • Dominar JavaScript asíncrono (James K. Nelson)