Las funciones asíncronas te permiten escribir un código basado en promesas como si fuese sincrónico.
Las funciones asincrónicas están habilitadas de manera predeterminada en Chrome, Edge, Firefox y Safari, y son, honestamente, maravillosas. Te permiten escribir un código basado en promesas como si fuese síncrono, pero sin bloquear el subproceso principal. Hacen que tu código asíncrono sea menos “inteligente” y más legible.
Las funciones asíncronas funcionan de la siguiente manera:
async function myFirstAsyncFunction() {
try {
const fulfilledValue = await promise;
} catch (rejectedValue) {
// …
}
}
Si usas la palabra clave async
antes de una definición de función, puedes usar await
dentro de la función. Cuando await
una promesa, la función se pausa de una forma que no bloquea hasta que la promesa se detenga. Si la promesa se completa, recuperas el valor. Si la promesa rechaza, se arroja el valor rechazado.
Navegadores compatibles
Ejemplo: Cómo registrar una recuperación
Supongamos que quieres recuperar una URL y registrar la respuesta como texto. Así se ve el uso de promesas:
function logFetch(url) {
return fetch(url)
.then((response) => response.text())
.then((text) => {
console.log(text);
})
.catch((err) => {
console.error('fetch failed', err);
});
}
Y aquí se ve lo mismo con el uso de funciones asíncronas:
async function logFetch(url) {
try {
const response = await fetch(url);
console.log(await response.text());
} catch (err) {
console.log('fetch failed', err);
}
}
Es la misma cantidad de líneas, pero desaparecen todas las devoluciones de llamada. Esto hace que sea más fácil de leer, especialmente para quienes conocen menos las promesas.
Valores de retorno asíncronos
Las funciones asincrónicas siempre muestran una promesa, ya sea que uses await
o no. Esa
promesa resuelve con lo que muestre la función asíncrona o rechaza con
lo que sea que arroje la función asíncrona. Entonces:
// wait ms milliseconds
function wait(ms) {
return new Promise((r) => setTimeout(r, ms));
}
async function hello() {
await wait(500);
return 'world';
}
…llamar a hello()
muestra una promesa que se completa con "world"
.
async function foo() {
await wait(500);
throw Error('bar');
}
…llamar a foo()
muestra una promesa que rechaza con Error('bar')
.
Ejemplo: Transmisión de una respuesta
El beneficio de las funciones asíncronas aumenta en ejemplos más complejos. Supongamos que quieres transmitir una respuesta mientras sales de los fragmentos y mostrar el tamaño final.
Así es con promesas:
function getResponseSize(url) {
return fetch(url).then((response) => {
const reader = response.body.getReader();
let total = 0;
return reader.read().then(function processResult(result) {
if (result.done) return total;
const value = result.value;
total += value.length;
console.log('Received chunk', value);
return reader.read().then(processResult);
});
});
}
Mira, Jake "poseedor de promesas" Archibald. ¿Ves cómo llamo a processResult()
dentro de sí mismo para configurar un bucle asíncrono? Escribir eso me hizo sentir muy inteligente. Pero como con la mayoría de los códigos "inteligentes", tienes que mirarlos fijamente durante años para descubrir qué hace, como esas imágenes estilo "ojo mágico" de los 90.
Intentémoslo de nuevo con funciones asíncronas:
async function getResponseSize(url) {
const response = await fetch(url);
const reader = response.body.getReader();
let result = await reader.read();
let total = 0;
while (!result.done) {
const value = result.value;
total += value.length;
console.log('Received chunk', value);
// get the next result
result = await reader.read();
}
return total;
}
Todo lo “inteligente” desapareció. El bucle asincrónico que me hizo tan presumido está reemplazado por un confiable y aburrido bucle while. Mucho mejor. En el futuro, obtendrás iteradores asíncronos, que reemplazarán el bucle while
por un bucle for-of, lo que lo hará aún más prolijo.
Otra sintaxis de función asíncrona
Ya te mostré async function() {}
, pero la palabra clave async
se puede
usar con otra sintaxis de función:
Funciones de flecha
// map some URLs to json-promises
const jsonPromises = urls.map(async (url) => {
const response = await fetch(url);
return response.json();
});
Métodos de objetos
const storage = {
async getAvatar(name) {
const cache = await caches.open('avatars');
return cache.match(`/avatars/${name}.jpg`);
}
};
storage.getAvatar('jaffathecake').then(…);
Métodos de clase
class Storage {
constructor() {
this.cachePromise = caches.open('avatars');
}
async getAvatar(name) {
const cache = await this.cachePromise;
return cache.match(`/avatars/${name}.jpg`);
}
}
const storage = new Storage();
storage.getAvatar('jaffathecake').then(…);
Debe tener cuidado. Evita generar demasiadas secuencias
A pesar de estar escribiendo un código que luce síncrono, asegúrate de no perder la oportunidad de hacer cosas en paralelo.
async function series() {
await wait(500); // Wait 500ms…
await wait(500); // …then wait another 500ms.
return 'done!';
}
Completar lo anterior lleva 1000 ms, mientras que
async function parallel() {
const wait1 = wait(500); // Start a 500ms timer asynchronously…
const wait2 = wait(500); // …meaning this timer happens in parallel.
await Promise.all([wait1, wait2]); // Wait for both timers in parallel.
return 'done!';
}
Lo anterior tarda 500 ms en completarse porque ambas esperas ocurren al mismo tiempo. Veamos un ejemplo práctico.
Ejemplo: Cómo generar recuperaciones en orden
Supongamos que deseas recuperar una serie de URLs y registrarlas lo antes posible, en el orden correcto.
Respira profundo: Así luce eso con promesas:
function markHandled(promise) {
promise.catch(() => {});
return promise;
}
function logInOrder(urls) {
// fetch all the URLs
const textPromises = urls.map((url) => {
return markHandled(fetch(url).then((response) => response.text()));
});
// log them in order
return textPromises.reduce((chain, textPromise) => {
return chain.then(() => textPromise).then((text) => console.log(text));
}, Promise.resolve());
}
Sí, así es, estoy usando reduce
para encadenar una secuencia de promesas. Soy muy inteligente. Pero esta es una codificación tan inteligente que es mejor no usarla.
Sin embargo, cuando conviertes lo anterior en una función asíncrona, es tentador generar demasiado secuenciales:
async function logInOrder(urls) {
for (const url of urls) {
const response = await fetch(url);
console.log(await response.text());
}
}
function markHandled(...promises) {
Promise.allSettled(promises);
}
async function logInOrder(urls) {
// fetch all the URLs in parallel
const textPromises = urls.map(async (url) => {
const response = await fetch(url);
return response.text();
});
markHandled(...textPromises);
// log them in sequence
for (const textPromise of textPromises) {
console.log(await textPromise);
}
}
Solución alternativa de compatibilidad con navegadores: generadores
Si apuntas a un perfil de navegadores que soportan generadores (que incluye la versión más reciente de todos los navegadores importante ), puedes casi usar polyfill para funciones asincrónicas.
Babel lo hará por ti. Este es un ejemplo a través de Babel REPL
- fíjate cuán similar es el código transpilado. Esta transformación forma parte del valor preestablecido es2017 de Babel.
Recomiendo el enfoque de transpilación, porque puedes apagarlo una vez que tus navegadores de destino admitan funciones asíncronas, pero, si realmente no quieres usar un transpilador, puedes tomar el polyfill de Babel y usarlo por tu cuenta. En lugar de esta sintaxis:
async function slowEcho(val) {
await wait(1000);
return val;
}
incluirías el polyfill y escribirías lo siguiente:
const slowEcho = createAsyncFunction(function* (val) {
yield wait(1000);
return val;
});
Ten en cuenta que tienes que pasar un generador (function*
) a createAsyncFunction
y usar yield
en lugar de await
. Aparte de eso, funciona de la misma forma.
Solución alternativa: regenerador
Si apuntas a un perfil de navegadores viejos, Babel también puede transpilar generadores, permitiéndote usar funciones asincrónicas hasta IE8. Para ello, necesitas el valor preestablecido es2017 de Babel y el valor preestablecido es2015.
El resultado no es tan bonito, así que presta atención al sobredimensionamiento de código.
¡Haz todo asincrónico!
Una vez que las funciones asincrónicas lleguen a todos los navegadores, ¡úsalas en todas las funciones que muestran promesas! No solo hacen que tu código sea más prolijo, sino que se asegura de que la función siempre muestre una promesa.
Me emocionaron mucho las funciones asíncronas en 2014 y es genial ver que se implementan, de verdad, en los navegadores. ¡Guau!