Le funzioni asincrone ti consentono di scrivere codice basato su promesse come se fosse sincrono.
Le funzioni asincrone sono attive per impostazione predefinita in Chrome, Edge, Firefox e Safari e sono davvero entusiasmanti. 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 della definizione di una 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 si compie, il valore verrà recuperato. 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 dal fatto che utilizzi await
o meno. 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 soddisfa 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 trasmettere una risposta in modalità flusso durante la disconnessione dei blocchi e restituire la dimensione finale.
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 che 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;
}
Tutto quello "smart" è andato. 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 del corso
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!';
}
Il completamento di quanto riportato sopra richiede 500 ms, perché entrambe le attese si verificano contemporaneamente. Vediamo un esempio pratico.
Esempio: output delle ricerche 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ì, esatto, sto usando reduce
per concatenare una sequenza di promesse. Sono tanto intelligente. Tuttavia, questo è un po' di 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 browser che supportano i generatori (che include l'ultima versione di tutti i principali browser ), puoi usare le funzioni asincrone di polyfill.
Babel lo farà per te. Ecco un esempio tramite la Babel REPL
- 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 il fatto, funziona allo stesso modo.
Soluzione alternativa: regenerator
Se hai scelto come target browser meno recenti, Babel può anche eseguire il transpile dei generatori, consentendoti di utilizzare funzioni asincrone fino a IE8. Per farlo, devi avere la preimpostazione es2017 di Babel e la 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 garantiscono che la funzione restituirà sempre una promessa.
Ho iniziato a interessarmi alle funzioni asincrone nel 2014 e ora è fantastico vederle finalmente disponibili nei browser. Accidenti!