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 asincrónicas 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 cumple, recibes el valor. Si la promesa rechaza, se arroja el valor rechazado.
Navegadores compatibles
Ejemplo: registra una recuperación
Supongamos que quieres recuperar una URL y registrar la respuesta como texto. Así se ve con 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 sea que muestre la función asíncrona o rechaza con lo que sea que arroje la función asíncrona. De modo que
// 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.
Aquí está 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
durante años para descubrir lo que hace, como esas imágenes
de ojo mágico de los 90.
Intentémoslo nuevamente 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, tendrás iteradores asíncronos, que reemplazarán el bucle while
con 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 objeto
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!';
}
Completar lo anterior lleva 500 ms, porque ambas esperas suceden al mismo tiempo. Veamos un ejemplo práctico.
Ejemplo: Cómo generar recuperaciones en orden
Supongamos que quieres obtener 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 convertimos lo anterior en una función asíncrona, es tentador generar demasiadas secuencias:
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. Aquí tienes un ejemplo mediante el REPL de Babel.
- 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 manera.
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 atractivo, así que presta atención al aumento de tamaño del código.
¡Haz todo asíncrono!
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 aseguran 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. ¡Uy!