Le funzioni asincrone consentono di scrivere codice basato sulla promessa come se fosse sincrono.
Le funzioni asincrone sono attive per impostazione predefinita in Chrome, Edge, Firefox e Safari e sono davvero entusiasmanti. 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 in questo 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 alla risoluzione della promessa. Se la promessa si compie, il valore verrà recuperato. Se la promessa viene rifiutata, viene lanciato il valore rifiutato.
Supporto del browser
Esempio: logging di un recupero
Supponiamo di voler recuperare un URL e registrare la risposta come testo. Ecco come si presenta usando le promesse:
function logFetch(url) {
return fetch(url)
.then((response) => response.text())
.then((text) => {
console.log(text);
})
.catch((err) => {
console.error('fetch failed', err);
});
}
La stessa cosa avviene quando si utilizzano 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);
}
}
Il numero di righe è lo stesso, ma tutte le richiamate sono state eliminate. In questo modo la lettura è più semplice, soprattutto per chi ha meno familiarità 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 qualsiasi valore restituito dalla funzione asincrona o rifiuta con
qualsiasi cosa venga generata dalla funzione asincrona. Quindi nel caso di:
// 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 di foo()
restituisce una promessa che rifiuta con Error('bar')
.
Esempio: streaming di una risposta
Il vantaggio delle funzioni asincrone aumenta in esempi più complessi. Supponiamo che tu voglia trasmettere una risposta in modalità flusso durante la disconnessione dei blocchi e restituire la dimensione finale.
Eccolo con 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);
});
});
}
Guardami, Jake "coltivatore di promesse" Archibald. Hai visto come sto chiamando
processResult()
al suo interno per configurare un loop asincrono? Scrivere che mi ha fatto sentire molto intelligente. Ma come la maggior parte degli smart code, bisogna osservarlo per
età per capire cosa sta facendo, come in una di quelle immagini degli occhi magici
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 ha fatto sentire
compiacito viene sostituito da un ciclo "atleta" affidabile e noioso. Decisamente meglio. In futuro, verranno utilizzati iteratori asincroni, che sostituiranno il loop while
con un loop for-of, rendendolo ancora più ordinato.
Sintassi di altra funzione asincrona
Ti ho già mostrato async function() {}
, ma la parola chiave async
può essere
utilizzata con altra sintassi delle funzioni:
Funzioni freccia
// map some URLs to json-promises
const jsonPromises = urls.map(async (url) => {
const response = await fetch(url);
return response.json();
});
Metodi relativi agli 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 un approccio troppo sequenziale
Anche se stai scrivendo codice che sembra sincrono, assicurati di non perdere l'opportunità di fare le cose in parallelo.
async function series() {
await wait(500); // Wait 500ms…
await wait(500); // …then wait another 500ms.
return 'done!';
}
Il completamento di quanto sopra 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 sopra richiede 500 ms, perché entrambe le attese si verificano contemporaneamente. Vediamo un esempio pratico.
Esempio: output dei recuperi in ordine
Supponiamo che tu voglia recuperare una serie di URL e registrarli il prima possibile, nell'ordine corretto.
Respiro profondo: ecco come si presenta con una promessa:
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 così intelligente. Ma questa è una programmazione così intelligente che è meglio fare senza.
Tuttavia, quando converti quanto riportato sopra in una funzione asincrona, la tentazione di passare 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 dei 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 è simile il codice trapelato. Questa trasformazione fa parte del preimpostazione es2017 di Babel.
Consiglio l'approccio di transpiling, perché può semplicemente disattivarlo una volta che i browser di destinazione supporteranno le funzioni asincrone, ma se veramente non vuoi utilizzare un transpiler, puoi utilizzare il polyfill di Babel e utilizzarlo autonomamente. 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 usare yield
anziché await
. A parte il fatto, funziona allo stesso modo.
Soluzione: rigeneratore
Se scegli come target browser meno recenti, Babel può anche eseguire il transpile dei generatori, consentendoti di utilizzare funzioni asincrone fino a IE8. Per farlo hai bisogno del preimpostazione es2017 di Babel e della preimpostazione es2015.
L'output non è molto bello, quindi fai attenzione al codice in eccesso.
Asincrone tutte le cose.
Quando le funzioni asincrone sono disponibili in tutti i browser, puoi utilizzarle per ogni funzione che restituisce una promessa. Non solo rendono il codice più ordinato, ma garantisce che la funzione restituisca sempre una promessa.
Mi ha davvero entusiasmato le funzioni asincrone nel 2014 ed è bello vederle arrivare nei browser. Accidenti!