Funzioni asincrone: fare promesse in modo semplice

Le funzioni asincrone ti consentono di scrivere codice basato su promesse come se fosse sincrono.

Jake Archibald
Jake Archibald

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

Supporto dei browser

  • Chrome: 55.
  • Edge: 15.
  • Firefox: 52.
  • Safari: 10.1.

Origine

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:

Non consigliato: troppo sequenziale
async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}
Sembra molto più ordinato, ma il secondo recupero non inizia finché il primo non è stato completamente letto e così via. Questo è molto più lento rispetto all'esempio che esegue i recuperi in parallelo. Fortunatamente esiste una via di mezzo ideale.
Consigliato: ben parallelo
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);
  }
}
In questo esempio, gli URL vengono recuperati e letti in parallelo, ma il bit "intelligente"reduce viene sostituito da un ciclo for standard, noioso e leggibile.

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

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!