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 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

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 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:

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. Questa operazione è molto più lenta rispetto all'esempio di promise che esegue i recuperi in parallelo. Fortunatamente, esiste una soluzione intermedia 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 reduce "intelligente" viene sostituito da un ciclo for standard, noioso e leggibile.

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

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!