Funzioni asincrone: fare promesse in modo semplice

Le funzioni asincrone consentono di scrivere codice basato sulla promessa come se fosse sincrono.

Giacomo Archibald
Jake Archibald

Le funzioni asincrone sono abilitate per impostazione predefinita in Chrome, Edge, Firefox e Safari e sono davvero meravigliose. Ti permettono 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 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 finché la promessa non si stabilizza. Se la promessa viene soddisfatta, ricevi il valore. Se la promessa viene rifiutata, viene generato il valore rifiutato.

Supporto del browser

Supporto dei browser

  • 55
  • 15
  • 52
  • 10.1

Fonte

Esempio: registrare un recupero

Supponiamo di voler recuperare un URL e registrare la risposta come testo. Ecco come si presenta utilizzando le promesse:

function logFetch(url) {
  return fetch(url)
    .then((response) => response.text())
    .then((text) => {
      console.log(text);
    })
    .catch((err) => {
      console.error('fetch failed', err);
    });
}

L'utilizzo delle funzioni asincrone è la stessa:

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 non sono più disponibili. In questo modo sarà più facile leggere, soprattutto per chi ha meno familiarità con le promesse.

Valori restituiti asincroni

Le funzioni asincrone restituiscono sempre una promessa, a prescindere dall'utilizzo di await. Tale promessa si risolve con qualsiasi cosa venga restituita dalla funzione asincrona, oppure con qualsiasi cosa venga rifiutata 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 di hello() restituisce una promessa che viene rispettata con "world".

async function foo() {
  await wait(500);
  throw Error('bar');
}

...la chiamata di foo() restituisce una promessa che rifiuta con Error('bar').

Esempio: trasmettere una risposta in streaming

Il vantaggio delle funzioni asincrone aumenta negli esempi più complessi. Supponiamo che tu voglia trasmettere una risposta in streaming uscendo dai 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 "detentore di promesse" Archibald. Vedi come chiamo processResult() dentro di se stesso per configurare un loop asincrono? Scrivere che mi ha fatto sentire molto intelligente. Come gran parte dei codici intelligenti, devi fissarlo per far capire alle età che cosa sta facendo, come una di quelle foto incantevoli 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ù niente di "intelligente". Il loop asincrono che mi ha fatto sentire così compiaciuta è stato sostituito da un loop di compiacimento affidabile e noioso. Decisamente meglio. In futuro, saranno disponibili iteratori asincroni, che sostituiranno il loop while con un loop for-of, rendendolo ancora più ordinato.

Altra sintassi di funzione asincrona

Ti ho già mostrato async function() {}, ma la parola chiave async può essere utilizzata con altre 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 degli oggetti

const storage = {
  async getAvatar(name) {
    const cache = await caches.open('avatars');
    return cache.match(`/avatars/${name}.jpg`);
  }
};

storage.getAvatar('jaffathecake').then(…);

Metodi della 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 optare per un approccio troppo sequenziale

Anche se stai scrivendo codice dall'aspetto sincrono, assicurati di non perdere l'opportunità di svolgere operazioni in parallelo.

async function series() {
  await wait(500); // Wait 500ms…
  await wait(500); // …then wait another 500ms.
  return 'done!';
}

Il completamento della procedura 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: eseguire l'output dei recuperi in ordine

Supponiamo che tu voglia recuperare una serie di URL e registrarli il prima possibile, nell'ordine corretto.

Fai un bel respiro, ecco che cosa accade con 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 collegare una sequenza di promesse. Sono così intelligente. Ma la programmazione è un po' così intelligente che non è possibile fare meglio.

Tuttavia, quando si converte quanto descritto sopra in una funzione asincrona, si potrebbe avere la tentazione di passare troppo sequenziali:

Non consigliato - troppo sequenziale
async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}
L'aspetto è molto più ordinato, ma il secondo recupero inizia solo dopo che il primo recupero è stato letto per intero e così via. Questo è molto più lento rispetto all'esempio di promesse che esegue i recuperi in parallelo. Fortunatamente c'è un terreno di mezzo ideale.
Consigliato - Simpatico e 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 "smart" reduce viene sostituito con 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 la versione più recente di tutti i browser principali), puoi ordinare le funzioni di polyfill asincroni.

Babel lo farà per te, ecco un esempio tramite Babel REPL

Consiglio l'approccio del traspilazione, perché è possibile disattivarlo quando i browser di destinazione supportano le funzioni asincrone, ma se davvero non vuoi utilizzare un transpiler, puoi prendere il polyfill di Babele 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 utilizzare yield anziché await. A parte questo, funziona allo stesso modo.

Soluzione alternativa: rigeneratore

Se scegli come target i browser meno recenti, Babel è in grado di eseguire anche il transpile dei generatori, consentendoti di utilizzare le funzioni asincrone fino a IE8. A questo scopo, hai bisogno della preimpostazione es2017 di Babele e della preimpostazione es2015.

L'output non è così bello, quindi fai attenzione al codebloat.

Sincronizza tutte le cose.

Una volta che le funzioni asincrone vengono visualizzate in tutti i browser, puoi utilizzarle in ogni funzione che restituisce promessa. Non solo rendono più ordinato il tuo codice, ma fanno sì che questa funzione restituisca sempre una promessa.

Mi sono veramente entusiasta delle funzioni asincrone nel 2014 ed è fantastico vederle apparire, davvero, nei browser. Peccato!