Esaminiamo Async/Await in JavaScript attraverso esempi.

L'autore dell'articolo analizza, attraverso esempi, Async/Await in JavaScript. In generale, Async/Await è un modo conveniente per scrivere codice asincrono. Prima di questa funzionalità, il codice asincrono si scriveva utilizzando callback e promesse. L'autore dell'articolo originale svela i vantaggi di Async/Await esaminando diversi esempi.

Ricordiamo: per tutti i lettori di «Habr» — sconto di 10.000 rubli per l'iscrizione a qualsiasi corso Skillbox con il codice promozionale «Habr».

Skillbox consiglia: Corso online educativo Un principio abbastanza semplice, la cui essenza è chiara dal nome: «Non ripeterti». Per un programmatore questo significa evitare codice duplicato e avere la possibilità di utilizzare l'astrazione nel lavoro..

Callback

Un callback è una funzione il cui richiamo è rinviato a un momento indefinito. In passato, i callback venivano utilizzati in quelle porzioni di codice in cui il risultato non poteva essere ottenuto immediatamente.

Ecco un esempio di lettura asincrona di un file in Node.js:

fs.readFile(__filename, 'utf-8', (err, data) => {
  if (err) {
    throw err;
  }
  console.log(data);
});

I problemi sorgono quando è necessario eseguire più operazioni asincrone contemporaneamente. Immaginiamo un tale scenario: viene effettuata una richiesta al database per l'utente Arfat, bisogna leggere il campo profile_img_url e caricare l'immagine dal server someserver.com.
Dopo il caricamento, convertiamo l'immagine in un altro formato, ad esempio da PNG a JPEG. Se la conversione ha avuto successo, una mail viene inviata all'utente. Successivamente, le informazioni sull'evento vengono registrate nel file transformations.log con la data specificata.

È importante prestare attenzione alla nidificazione delle callback e all'elevato numero di }) nella parte finale del codice. Questo è chiamato Callback Hell o Pyramid of Doom.

Gli svantaggi di questo approccio sono evidenti:

  • Questo codice è difficile da leggere.
  • È anche difficile gestire gli errori, il che spesso porta a una riduzione della qualità del codice.

Per risolvere questo problema, in JavaScript sono stati introdotti i promise. Essi permettono di sostituire la profonda nidificazione delle callback con la parola .then.

Un aspetto positivo dei promise è che rendono il codice molto più leggibile, andando dall'alto verso il basso, piuttosto che da sinistra a destra. Tuttavia, anche i promise hanno i loro problemi:

  • È necessario aggiungere un elevato numero di .then.
  • Al posto di try/catch, si usa .catch per gestire tutti gli errori.
  • Lavorare con più promise in un unico ciclo non è sempre comodo e, in alcuni casi, può complicare il codice.

Ecco un compito che mostrerà l'importanza dell'ultimo punto.

Supponiamo di avere un ciclo for che stampa una sequenza di numeri da 0 a 10 con un intervallo casuale (da 0 a n secondi). Utilizzando le promesse, dobbiamo modificare questo ciclo in modo che i numeri vengano stampati in ordine da 0 a 10. Così, se la stampa di zero richiede 6 secondi e quella di uno 2 secondi, prima verrà stampato zero e poi inizierà il conteggio per la stampa di uno.

E naturalmente, per risolvere questo problema non utilizziamo Async/Await o .sort. Un esempio di soluzione è alla fine.

Funzioni async

L'aggiunta delle funzioni async in ES2017 (ES8) ha semplificato il lavoro con le promesse. Sottolineo che le funzioni async operano "sopra" le promesse. Queste funzioni non rappresentano concetti qualitativamente diversi. Le funzioni async sono state progettate come un'alternativa al codice che utilizza le promesse.

Async/Await consente di organizzare il lavoro con codice asincrono in uno stile sincrono.

Pertanto, conoscere le promesse facilita la comprensione dei principi di Async/Await.

Sintassi

In una situazione normale, consiste in due parole chiave: async e await. La prima parola trasforma la funzione in una funzione asincrona. In queste funzioni è consentito l'uso di await. In qualsiasi altro caso, l'utilizzo di questa funzione genererà un errore.

// With function declaration
 
async function myFn() {
  // await ...
}
 
// With arrow function
 
const myFn = async () => {
  // await ...
}
 
function myFn() {
  // await fn(); (Syntax Error since no async)
}
 

Async viene inserito all'inizio della dichiarazione della funzione, e nel caso di una funzione freccia — tra il segno «=» e le parentesi.

Queste funzioni possono essere collocate in un oggetto come metodi oppure utilizzate nella dichiarazione di una classe.

// As an object's method
 
const obj = {
  async getName() {
    return fetch('https://www.example.com');
  }
}
 
// In a class
 
class Obj {
  async getResource() {
    return fetch('https://www.example.com');
  }
}

NB! È importante tenere a mente che i costruttori di classe e i getter/setter non possono essere asincroni.

Semantica e regole di esecuzione

Le funzioni async sono fondamentalmente simili alle funzioni JS standard, ma ci sono delle eccezioni.

Infatti, le funzioni async restituiscono sempre promesse:

async function fn() {
  return 'hello';
}
fn().then(console.log)
// hello

In particolare, fn restituisce la stringa hello. Poiché questa è una funzione asincrona, il valore della stringa viene incapsulato in una promessa utilizzando il costruttore.

Ecco una costruzione alternativa senza Async:

function fn() {
  return Promise.resolve('hello');
}

fn().then(console.log);
// hello

In questo caso, il ritorno della promessa è effettuato "manualmente". Una funzione asincrona è sempre incapsulata in una nuova promessa.

Se il valore restituito è un primitivo, la funzione async restituisce il valore avvolgendolo in una promessa. Se il valore restituito è già un oggetto promessa, il suo risultato viene restituito in una nuova promessa.

const p = Promise.resolve('hello')
p instanceof Promise;
// true

Promise.resolve(p) === p;
// true
 

Ma cosa succede se all'interno di una funzione asincrona si verifica un errore?

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

foo().catch(console.log);

Se non viene gestito, foo() restituirà una promessa con un rifiuto. In questa situazione, invece di Promise.resolve, verrà restituito Promise.reject contenente l'errore.

Le funzioni async restituiscono sempre una promessa, indipendentemente da ciò che viene restituito.

Le funzioni asincrone si sospendono ad ogni await.

Await influisce sulle espressioni. Quindi, se l'espressione è una promessa, la funzione async si sospende fino all'esecuzione della promessa. Se l'espressione non è una promessa, viene convertita in una promessa tramite Promise.resolve e poi completata.

// utility function to cause delay
// and get random value
 
const delayAndGetRandom = (ms) => {
  return new Promise(resolve => setTimeout(
    () => {
      const val = Math.trunc(Math.random() * 100);
      resolve(val);
    }, ms
  ));
};
 
async function fn() {
  const a = await 9;
  const b = await delayAndGetRandom(1000);
  const c = await 5;
  await delayAndGetRandom(1000);
 
  return a + b * c;
}
 
// Execute fn
fn().then(console.log);

Ecco una descrizione di come funziona la funzione fn.

  • Dopo la sua invocazione, la prima riga viene convertita da const a = await 9; in const a = await Promise.resolve(9);.
  • Dopo aver utilizzato Await, l'esecuzione della funzione si sospende fino a quando non ottiene il proprio valore (in questa situazione, è 9).
  • delayAndGetRandom(1000) sospende l'esecuzione della funzione fn fino a quando non si completa (dopo 1 secondo). Questo è fondamentalmente un arresto della funzione fn per 1 secondo.
  • delayAndGetRandom(1000) restituisce tramite resolve un valore casuale, che viene poi assegnato alla variabile b.
  • Il caso della variabile c è analogo a quello della variabile a. Dopo questo, tutto si ferma per un secondo, ma ora delayAndGetRandom(1000) non restituisce nulla, poiché non è necessario.
  • Alla fine, i valori vengono calcolati secondo la formula a + b * c. Il risultato viene incapsulato in una promessa tramite Promise.resolve e restituito dalla funzione.

Queste pause possono ricordare i generatori in ES6, ma ci sono motivi per questo.

Affrontiamo il problema

Ora consideriamo la soluzione al problema che è stata menzionata sopra.

Nella funzione finishMyTask viene utilizzato Await per attendere i risultati di operazioni come queryDatabase, sendEmail, logTaskInFile e altre. Se si confronta questa soluzione con quella in cui venivano utilizzate le promesse, diventa evidente la somiglianza. Tuttavia, la versione con Async/Await semplifica notevolmente tutte le complessità sintattiche. In questo caso, non ci sono molte callback e catene come .then/.catch.

Ecco una soluzione con l'output dei numeri, ci sono due varianti.

const wait = (i, ms) => new Promise(resolve => setTimeout(() => resolve(i), ms));
 
// Implementation One (Using for-loop)
const printNumbers = () => new Promise((resolve) => {
  let pr = Promise.resolve(0);
  for (let i = 1; i <= 10; i += 1) {
    pr = pr.then((val) => {
      console.log(val);
      return wait(i, Math.random() * 1000);
    });
  }
  resolve(pr);
});
 
// Implementation Two (Using Recursion)
 
const printNumbersRecursive = () => {
  return Promise.resolve(0).then(function processNextPromise(i) {
 
    if (i === 10) {
      return undefined;
    }
 
    return wait(i, Math.random() * 1000).then((val) => {
      console.log(val);
      return processNextPromise(i + 1);
    });
  });
};

Ecco una soluzione utilizzando funzioni async.

async function printNumbersUsingAsync() {
  for (let i = 0; i < 10; i++) {
    await wait(i, Math.random() * 1000);
    console.log(i);
  }
}

Gestione degli errori

Gli errori non gestiti vengono racchiusi in una promessa rifiutata. Tuttavia, nelle funzioni async, è possibile utilizzare la costruzione try/catch per eseguire la gestione degli errori in modo sincrono.

async function canRejectOrReturn() {
  \\ wait one second
  await new Promise(res => setTimeout(res, 1000));
 
  \\ Rifiuta con ~50% di probabilità
  if (Math.random() > 0.5) {
    throw new Error('Spiacente, numero troppo grande.')
  }
 
  return 'numero perfetto';
}

canRejectOrReturn() è una funzione asincrona che può completarsi con successo ('numero perfetto') oppure fallire con un errore ('Spiacente, numero troppo grande').

async function foo() {
  try {
    await canRejectOrReturn();
  } catch (e) {
    return 'errore catturato';
  }
}

Poiché nell'esempio sopra ci si aspetta l'esecuzione di canRejectOrReturn, un'uscita non riuscita comporterà l'esecuzione del blocco catch. Di conseguenza, la funzione foo terminerà o con undefined (quando non viene restituito nulla nel blocco try) o con 'error caught'. Pertanto, questa funzione non avrà un'uscita non riuscita, poiché il try/catch si occuperà dell'elaborazione della funzione foo stessa.

Ecco un altro esempio:

async function foo() {
  try {
    return canRejectOrReturn();
  } catch (e) {
    return 'error caught';
  }
}

Vale la pena notare che nell'esempio foo restituisce canRejectOrReturn. Foo in questo caso termina con un numero perfetto o restituisce l'errore Error (“Spiacente, numero troppo grande”). Il blocco catch non verrà mai eseguito.

Il problema è che foo restituisce una promessa, trasmessa da canRejectOrReturn. Pertanto, la soluzione della funzione foo diventa la soluzione per canRejectOrReturn. In questo caso, il codice sarà composto da sole due righe:

try {
    const promise = canRejectOrReturn();
    return promise;
}

Ecco cosa accade se si utilizzano insieme await e return:

async function foo() {
  try {
    return await canRejectOrReturn();
  } catch (e) {
    return 'error caught';
  }
}

Nel codice sopra, foo terminerà con un numero perfetto e con un errore catturato. Non ci saranno rifiuti. Tuttavia, foo terminerà con canRejectOrReturn, e non con undefined. Assicuriamoci di questo rimuovendo la riga return await canRejectOrReturn():

try {
    const value = await canRejectOrReturn();
    return value;
}
// …

Errori comuni e insidie

In alcuni casi, l'uso di Async/Await può portare a errori.

Await dimenticato

Questo accade abbastanza frequentemente: prima della promessa si dimentica la parola chiave await:

async function foo() {
  try {
    canRejectOrReturn();
  } catch (e) {
    return 'caught';
  }
}

Nel codice, come si può notare, non ci sono né await né return. Pertanto, foo termina sempre con undefined senza un ritardo di 1 secondo. Tuttavia, la promessa verrà eseguita. Se produce un errore o viene rifiutata, in tal caso verrà generato un UnhandledPromiseRejectionWarning.

Funzioni Async nei callback

Le funzioni Async vengono frequentemente utilizzate in .map o .filter come callback. Un esempio può essere la funzione fetchPublicReposCount(username), che restituisce il numero di repository pubblici aperti su GitHub. Supponiamo ci siano tre utenti di cui abbiamo bisogno delle statistiche. Ecco il codice per questo compito:

const url = 'https://api.github.com/users';

// Funzione utile per recuperare il conteggio dei repo
const fetchPublicReposCount = async (username) => {
  const response = await fetch(`${url}/${username}`);
  const json = await response.json();
  return json['public_repos'];
}

Abbiamo bisogno degli account ArfatSalman, octocat, norvig. In questo caso eseguiamo:

const users = [
  'ArfatSalman',
  'octocat',
  'norvig'
];

const counts = users.map(async username => {
  const count = await fetchPublicReposCount(username);
  return count;
});

Vale la pena notare l'uso di Await nella callback .map. Qui counts è un array di promesse, mentre .map è una callback anonima per ogni utente specificato.

Uso eccessivo e sequenziale di await

Prendiamo come esempio il seguente codice:

async function fetchAllCounts(users) {
  const counts = [];
  for (let i = 0; i < users.length; i++) {
    const username = users[i];
    const count = await fetchPublicReposCount(username);
    counts.push(count);
  }
  return counts;
}

Qui nella variabile count viene memorizzato il numero dei repo, poi questo numero viene aggiunto all'array counts. Il problema del codice è che finché non arrivano i dati dal server per il primo utente, tutti gli utenti successivi saranno in attesa. Pertanto, in un dato momento viene trattato solo un utente.

Se, ad esempio, il tempo di elaborazione per un utente è di circa 300 ms, allora per tutti gli utenti si parla già di un secondo; il tempo impiegato dipende in modo lineare dal numero di utenti. Poiché il recupero del numero di repository è indipendente l'uno dall'altro, i processi possono essere parallelizzati. Per questo serve lavorare con .map e Promise.all:

async function fetchAllCounts(users) {
  const promises = users.map(async username => {
    const count = await fetchPublicReposCount(username);
    return count;
  });
  return Promise.all(promises);
}

Promise.all riceve un array di promesse e restituisce una promessa. Questa si risolve solo dopo che tutte le promesse nell'array si sono completate, oppure si rifiuta al primo errore. È possibile che non tutte vengano avviate contemporaneamente; per garantire l'avvio simultaneo, si può utilizzare p-map.

Conclusione

Le funzioni asincrone stanno diventando sempre più importanti nello sviluppo. E per un utilizzo adattivo delle funzioni asincrone, vale la pena utilizzare Async Iterators. Un sviluppatore JavaScript deve avere una buona padronanza di questo.

Skillbox consiglia:

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster