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 asinc sono attive per impostazione predefinita in Chrome, Edge, Firefox e Safari e sono davvero meravigliose. Consentono di scrivere codice basato su promesse se fosse sincrono, ma senza bloccare il thread principale. Fanno un codice asincrono meno "intelligente" e più leggibili.

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 da non bloccarlo, fino a quando la promessa non si risolve. Se la promessa si mantiene, per riavere il valore. Se la promessa viene rifiutata, viene lanciato il valore rifiutato.

Supporto browser

Supporto dei browser

  • Chrome: 55.
  • Edge: 15.
  • Firefox: 52.
  • Safari: 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 più facili da leggere, 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. Questo promessa si risolve con qualsiasi cosa restituisca la funzione asincrona o rifiuta con il valore generato 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 di volere per trasmettere una risposta mentre si disconnesse i 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. Guarda come sto chiamando processResult() al suo interno per configurare un loop asincrono? Un testo che ha fruttato mi sento molto intelligente. Ma come la maggior parte degli "smart" il codice, devi fissarlo per capire cosa sta facendo, come una di quelle immagini dall'occhio magico da 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;
}

Tutti i dispositivi "intelligenti" non c'è più. Il loop asincrono che mi ha fatto sentire così compiaciuta è e poi sostituito con un ciclo assurdo affidabile e noioso. Decisamente meglio. In futuro, riceverai iteratori asincroni, il che sostituisci 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 la sintassi di altre 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 perderti 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 nella 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, potresti avere la tentazione 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ù ordinato, ma il mio secondo recupero non inizia finché non è stato completato completamente lette e così via. Questo è molto più lento rispetto all'esempio delle promesse secondo cui 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, reduce bit viene sostituito da un ciclo for standard, noioso e leggibile.
di Gemini Advanced.

Soluzione alternativa per il supporto dei browser: generatori

Se scegli come target browser che supportano i generatori (che includono all'ultima versione di tutti i principali browser ) puoi creare una sorta di polyfill con funzioni asincrone.

Babel lo farà per te, Ecco un esempio tramite la replica Babel

di Gemini Advanced.

Consiglio l'approccio transpiling, perché può essere disattivato solo una volta I browser di destinazione supportano le funzioni asincrone, ma se non vuoi utilizzare una funzione transcompilatore, puoi prendere polyfill di Babel e usarlo autonomamente. Invece di:

async function slowEcho(val) {
  await wait(1000);
  return val;
}

... dovresti includere il polyfill e scrivi:

const slowEcho = createAsyncFunction(function* (val) {
  yield wait(1000);
  return val;
});

Tieni presente che devi passare un generatore (function*) a createAsyncFunction, e utilizza yield anziché await. A parte il fatto, funziona allo stesso modo.

Soluzione: rigeneratore

Se scegli come target browser meno recenti, Babel può anche trapelare i generatori, consentendo di utilizzare funzioni asincrone fino a IE8. Per farlo, ti serve Preimpostazione es2017 di Babel e la preimpostazione es2015.

L'output non è molto bello, quindi fai attenzione eccesso di codice.

Asincrone tutte le cose.

Quando le funzioni asincrone sono disponibili in tutti i browser, utilizzale con una funzione promessa che restituisce come risultato. Non solo rendono più ordinato il codice, ma assicurati che questa funzione restituisca sempre una promessa.

Mi sono davvero entusiasta delle funzioni asincrone quando 2014 e è fantastico vederli arrivare, sul serio, nei browser. Accidenti!