Funzioni asincrone: fare promesse in modo semplice

Le funzioni asincrone consentono di scrivere codice basato sulla promessa 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. 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

Supporto dei browser

  • 55
  • 15
  • 52
  • 10.1

Origine

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:

Sconsigliato: troppo sequenziale
async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}
Sembra molto più curato, ma il mio secondo recupero non inizia finché il primo recupero non è stato letto completamente e così via. Questo è molto più lento rispetto all'esempio che esegue i recuperi in parallelo. Per fortuna esiste una via di mezzo ideale.
Opzione consigliata: in linea e in linea
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 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

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!