Le funzioni asincrone ti consentono di scrivere codice basato su promesse come se fosse sincrono.
Le funzioni asincrone sono attivate per impostazione predefinita in Chrome, Edge, Firefox e Safari e sono francamente meravigliose. Ti consentono di scrivere codice basato su promesse come se fosse sincrono, ma senza bloccare il thread principale. Rendono il codice asincrono meno "intelligente" e più leggibile.
Le funzioni asincrone funzionano nel seguente modo:
async function myFirstAsyncFunction() {
try {
const fulfilledValue = await promise;
} catch (rejectedValue) {
// …
}
}
Se utilizzi la parola chiave async
prima di una definizione di funzione, puoi utilizzare
await
all'interno della funzione. Quando await
una promessa, la funzione viene messa in pausa
in modo non bloccante fino al completamento della promessa. Se la promessa viene soddisfatta, ricevi il valore. Se la promessa viene rifiutata, viene generato il valore rifiutato.
Supporto browser
Esempio: registrazione di un recupero
Supponiamo che tu voglia recuperare un URL e registrare la risposta come testo. Ecco come funziona con le promesse:
function logFetch(url) {
return fetch(url)
.then((response) => response.text())
.then((text) => {
console.log(text);
})
.catch((err) => {
console.error('fetch failed', err);
});
}
Ed ecco la stessa cosa utilizzando le funzioni asincrone:
async function logFetch(url) {
try {
const response = await fetch(url);
console.log(await response.text());
} catch (err) {
console.log('fetch failed', err);
}
}
È lo stesso numero di righe, ma non ci sono più tutti i callback. In questo modo è molto più facile da leggere, soprattutto per chi ha meno dimestichezza con le promesse.
Valori restituiti asincroni
Le funzioni asincrone restituiscono sempre una promessa, indipendentemente dall'utilizzo o meno di await
. Questa promessa viene risolta con il valore restituito dalla funzione asincrona o rifiutata con il valore generato dalla funzione asincrona. Quindi, con:
// wait ms milliseconds
function wait(ms) {
return new Promise((r) => setTimeout(r, ms));
}
async function hello() {
await wait(500);
return 'world';
}
…la chiamata a hello()
restituisce una promessa che si adempie con "world"
.
async function foo() {
await wait(500);
throw Error('bar');
}
…la chiamata a foo()
restituisce una promessa che rifiuta con Error('bar')
.
Esempio: streaming di una risposta
Il vantaggio delle funzioni asincrone aumenta negli esempi più complessi. Supponiamo che tu voglia eseguire lo streaming di una risposta durante il logging dei chunk e restituire le dimensioni finali.
Ecco le promesse:
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);
});
});
}
Dai un'occhiata a me, Jake "padrone delle promesse" Archibald. Hai notato come chiamo
processResult()
all'interno di se stesso per configurare un ciclo asincrono? Scrivere mi ha fatto sentire molto intelligente. Ma, come la maggior parte del codice "intelligente", devi guardarlo per molto tempo per capire cosa fa, come una di quelle immagini magiche degli anni '90.
Riproviamo con le funzioni asincrone:
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;
}
Non c'è più nulla di "smart". Il ciclo asincrono che mi faceva sentire così soddisfatto è stato sostituita da un affidabile e noioso ciclo while. Decisamente meglio. In futuro, avrai accesso agli
iteratori asincroni,
che
sostituiranno il ciclo while
con un ciclo for-of, rendendolo ancora più ordinato.
Altra sintassi delle funzioni asincrone
Ti ho già mostrato async function() {}
, ma la parola chiave async
può essere impiegata con un'altra sintassi di funzione:
Funzioni freccia
// map some URLs to json-promises
const jsonPromises = urls.map(async (url) => {
const response = await fetch(url);
return response.json();
});
Metodi degli oggetti
const storage = {
async getAvatar(name) {
const cache = await caches.open('avatars');
return cache.match(`/avatars/${name}.jpg`);
}
};
storage.getAvatar('jaffathecake').then(…);
Metodi di classe
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(…);
Attenzione: Evita di essere troppo sequenziale
Anche se stai scrivendo codice che sembra sincrono, assicurati di non perdere l'opportunità di eseguire operazioni in parallelo.
async function series() {
await wait(500); // Wait 500ms…
await wait(500); // …then wait another 500ms.
return 'done!';
}
L'operazione precedente richiede 1000 ms, mentre:
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!';
}
L'operazione precedente richiede 500 ms per essere completata, perché entrambe le attese si verificano contemporaneamente. Vediamo un esempio pratico.
Esempio: output delle selezioni in ordine
Supponiamo che tu voglia recuperare una serie di URL e registrarli il prima possibile nell'ordine corretto.
Respiro profondo: ecco come funzionano le promesse:
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ì, è corretto, sto utilizzando reduce
per concatenare una sequenza di promesse. Sono tanto intelligente. Tuttavia, si tratta di un codice così intelligente che è meglio evitare.
Tuttavia, quando si converte il codice precedente in una funzione asincrona, è facile cadere nella tentazione di procedere in modo troppo sequenziale:
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); } }
Soluzione alternativa per il supporto del browser: generatori
Se scegli come target i browser che supportano i generatori (tra cui la versione più recente di tutti i browser principali ), puoi eseguire il polyfill delle funzioni asincrone.
Babel lo farà per te. Ecco un esempio tramite la REPL di Babel
- nota quanto sia simile il codice transpiled. Questa trasformazione fa parte del preset es2017 di Babel.
Consiglio l'approccio di transpiling, perché puoi disattivarlo quando i browser di destinazione supportano le funzioni asincrone, ma se davvero non vuoi utilizzare un transpiler, puoi utilizzare il polyfill di Babel. Invece di:
async function slowEcho(val) {
await wait(1000);
return val;
}
…dovresti includere il polyfill e scrivere:
const slowEcho = createAsyncFunction(function* (val) {
yield wait(1000);
return val;
});
Tieni presente che devi passare un generatore (function*
) a createAsyncFunction
e utilizzare yield
anziché await
. A parte questo, funziona allo stesso modo.
Soluzione alternativa: regenerator
Se scegli come target browser meno recenti, Babel può anche transpilare i generatori, consentendoti di utilizzare funzioni asincrone fino a IE8. Per farlo, hai bisogno della preimpostazione es2017 di Babel e della preimpostazione es2015.
L'output non è così bello, quindi fai attenzione al bloated code.
Async per tutto!
Una volta che le funzioni asincrone saranno disponibili su tutti i browser, potrai utilizzarle in ogni funzione che restituisce una promessa. Non solo rendono il codice più ordinato, ma assicurano che la funzione sempre restituisca una promessa.
Ho iniziato a interessarmi alle funzioni asincrone nel 2014 e mi fa molto piacere vederle finalmente disponibili nei browser. Evviva!