Diamo un'occhiata ad Async/Await in JavaScript utilizzando esempi

L'autore dell'articolo esamina esempi di Async/Await in JavaScript. Nel complesso, Async/Await è un modo conveniente per scrivere codice asincrono. Prima che apparisse questa funzionalità, tale codice veniva scritto utilizzando callback e promesse. L'autore dell'articolo originale rivela i vantaggi di Async/Await analizzando vari esempi.

Ti ricordiamo: per tutti i lettori di "Habr" - uno sconto di 10 rubli al momento dell'iscrizione a qualsiasi corso Skillbox utilizzando il codice promozionale "Habr".

Skillbox consiglia: Corso didattico online "Sviluppatore Java".

Richiamata

La callback è una funzione la cui chiamata viene ritardata indefinitamente. In precedenza, i callback venivano utilizzati in quelle aree del 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 questo scenario: viene effettuata una richiesta al database utenti Arfat, è necessario leggere il suo campo profile_img_url e scaricare un'immagine dal server someserver.com.
Dopo il download, convertiamo l'immagine in un altro formato, ad esempio da PNG a JPEG. Se la conversione ha avuto esito positivo, viene inviata una lettera all'e-mail dell'utente. Successivamente, le informazioni sull'evento vengono inserite nel file trasformations.log, indicando la data.

Vale la pena prestare attenzione alla sovrapposizione dei callback e al gran numero di }) nella parte finale del codice. Si chiama Callback Hell o Pyramid of Doom.

Gli svantaggi di questo metodo sono evidenti:

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

Per risolvere questo problema, sono state aggiunte le promesse a JavaScript. Ti consentono di sostituire l'annidamento profondo dei callback con la parola .then.

L'aspetto positivo delle promesse è che rendono il codice molto più leggibile, dall'alto verso il basso anziché da sinistra a destra. Tuttavia, le promesse hanno anche i loro problemi:

  • È necessario aggiungere molti .then.
  • Invece di try/catch, per gestire tutti gli errori viene utilizzato .catch.
  • Lavorare con più promesse all'interno di un ciclo non è sempre conveniente; in alcuni casi complicano il codice.

Ecco un problema che mostrerà il significato dell'ultimo punto.

Supponiamo di avere un ciclo for che stampa una sequenza di numeri da 0 a 10 a intervalli casuali (0–n secondi). Utilizzando le promesse, è necessario modificare questo ciclo in modo che i numeri vengano stampati in sequenza da 0 a 10. Quindi, se occorrono 6 secondi per stampare uno zero e 2 secondi per stampare un uno, lo zero dovrebbe essere stampato prima, quindi inizierà il conto alla rovescia per la stampa di quello.

E ovviamente non utilizziamo Async/Await o .sort per risolvere questo problema. Alla fine c'è una soluzione di esempio.

Funzioni asincrone

L'aggiunta di funzioni asincrone in ES2017 (ES8) ha semplificato il compito di lavorare con le promesse. Noto che le funzioni asincrone funzionano "sopra" le promesse. Queste funzioni non rappresentano concetti qualitativamente diversi. Le funzioni asincrone sono intese come 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 rende più semplice comprendere i principi di Async/Await.

sintassi

Normalmente è composto da due parole chiave: async e wait. La prima parola trasforma la funzione in asincrona. Tali funzioni consentono l'uso di wait. In ogni 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 inserite in un oggetto come metodi o utilizzate in una dichiarazione di 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');
  }
}

ATTENZIONE! Vale la pena ricordare che i costruttori di classi e i getter/setter non possono essere asincroni.

Semantica e regole di esecuzione

Le funzioni asincrone sono fondamentalmente simili alle funzioni JS standard, ma esistono delle eccezioni.

Pertanto, le funzioni asincrone restituiscono sempre promesse:

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

Nello specifico, fn restituisce la stringa ciao. Bene, poiché si tratta di una funzione asincrona, il valore della stringa è racchiuso in una promessa utilizzando un costruttore.

Ecco un design alternativo senza Async:

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

In questo caso la promessa viene restituita “manualmente”. Una funzione asincrona è sempre avvolta in una nuova promessa.

Se il valore restituito è una primitiva, la funzione asincrona restituisce il valore racchiudendolo in una promessa. Se il valore restituito è un oggetto promessa, la sua risoluzione viene restituita in una nuova promessa.

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

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

async function foo() {
  throw Error('bar');
}
 
foo().catch(console.log);

Se non viene elaborato, foo() restituirà una promessa con un rifiuto. In questa situazione, verrà restituito Promise.reject contenente un errore anziché Promise.resolve.

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

Le funzioni asincrone si fermano ad ogni wait.

L'attesa colpisce le espressioni. Pertanto, se l'espressione è una promessa, la funzione asincrona viene sospesa finché la promessa non viene mantenuta. Se l'espressione non è una promessa, viene convertita in promessa tramite Promise.resolve e quindi 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);

Ed ecco una descrizione di come funziona la funzione fn.

  • Dopo averlo chiamato, la prima riga viene convertita da const a = wait 9; in const a = attendono Promise.resolve(9);.
  • Dopo aver utilizzato Await, l'esecuzione della funzione viene sospesa finché a non ottiene il suo valore (nella situazione attuale è 9).
  • delayAndGetRandom(1000) sospende l'esecuzione della funzione fn finché non si completa (dopo 1 secondo). Ciò interrompe effettivamente la funzione fn per 1 secondo.
  • delayAndGetRandom(1000) tramite risoluzione restituisce un valore casuale, che viene quindi assegnato alla variabile b.
  • Ebbene, il caso della variabile c è simile al caso della variabile a. Dopodiché, tutto si ferma per un secondo, ma ora delayAndGetRandom(1000) non restituisce nulla perché non è richiesto.
  • Di conseguenza, i valori vengono calcolati utilizzando la formula a + b * c. Il risultato è racchiuso in una promessa utilizzando Promise.resolve e restituito dalla funzione.

Queste pause potrebbero ricordare i generatori di ES6, ma c'è qualcosa di vero le tue ragioni.

Risolvere il problema

Bene, ora diamo un'occhiata alla soluzione al problema sopra menzionato.

La funzione finishMyTask utilizza Await per attendere i risultati di operazioni come queryDatabase, sendEmail, logTaskInFile e altre. Se confronti questa soluzione con quella in cui sono state utilizzate le promesse, le somiglianze diventeranno evidenti. Tuttavia, la versione Async/Await semplifica notevolmente tutte le complessità sintattiche. In questo caso, non c'è un gran numero di callback e catene come .then/.catch.

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

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);
    });
  });
};

Ed ecco una soluzione che utilizza le funzioni asincrone.

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

Errore di elaborazione

Gli errori non gestiti sono racchiusi in una promessa rifiutata. Tuttavia, le funzioni asincrone possono utilizzare try/catch per gestire gli errori in modo sincrono.

async function canRejectOrReturn() {
  // wait one second
  await new Promise(res => setTimeout(res, 1000));
 
// Reject with ~50% probability
  if (Math.random() > 0.5) {
    throw new Error('Sorry, number too big.')
  }
 
return 'perfect number';
}

canRejectOrReturn() è una funzione asincrona che riesce ("numero perfetto") o fallisce con un errore ("Scusa, numero troppo grande").

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

Poiché l'esempio precedente prevede l'esecuzione di canRejectOrReturn, il suo stesso fallimento comporterà l'esecuzione del blocco catch. Di conseguenza, la funzione foo terminerà con unfine (quando non viene restituito nulla nel blocco try) o con un errore rilevato. Di conseguenza, questa funzione non fallirà perché try/catch gestirà la funzione foo stessa.

Ecco un altro esempio:

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

Vale la pena prestare attenzione al fatto che nell'esempio canRejectOrReturn viene restituito da foo. Foo in questo caso termina con un numero perfetto o restituisce un errore ("Scusa, numero troppo grande"). Il blocco catch non verrà mai eseguito.

Il problema è che foo restituisce la promessa passata da canRejectOrReturn. Quindi la soluzione per foo diventa la soluzione per canRejectOrReturn. In questo caso il codice sarà composto solo da due righe:

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

Ecco cosa succede se usi wait e return insieme:

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

Nel codice sopra, foo uscirà con successo sia con un numero perfetto che con un errore rilevato. Non ci saranno rifiuti qui. Ma foo restituirà con canRejectOrReturn, non con unfine. Assicuriamoci di ciò rimuovendo la riga return wait canRejectOrReturn():

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

Errori e insidie ​​comuni

In alcuni casi, l'utilizzo di Async/Await può causare errori.

Attesa dimenticata

Ciò accade abbastanza spesso: la parola chiave wait viene dimenticata prima della promessa:

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

Come puoi vedere, non c'è attesa o restituzione nel codice. Pertanto foo esce sempre con unfine senza un ritardo di 1 secondo. Ma la promessa verrà mantenuta. Se genera un errore o un rifiuto, verrà chiamato UnhandledPromiseRejectionWarning.

Funzioni asincrone nelle richiamate

Le funzioni asincrone vengono spesso utilizzate in .map o .filter come callback. Un esempio è la funzione fetchPublicReposCount(username), che restituisce il numero di repository aperti su GitHub. Supponiamo che ci siano tre utenti di cui abbiamo bisogno delle metriche. Ecco il codice per questa attività:

const url = 'https://api.github.com/users';
 
// Utility fn to fetch repo counts
const fetchPublicReposCount = async (username) => {
  const response = await fetch(`${url}/${username}`);
  const json = await response.json();
  return json['public_repos'];
}

Abbiamo bisogno dei conti ArfatSalman, Octocat e Norvig. In questo caso facciamo:

const users = [
  'ArfatSalman',
  'octocat',
  'norvig'
];
 
const counts = users.map(async username => {
  const count = await fetchPublicReposCount(username);
  return count;
});

Vale la pena prestare attenzione ad Await nel callback .map. Qui conta una serie di promesse e .map è un callback anonimo per ciascun utente specificato.

Uso eccessivamente coerente di wait

Prendiamo come esempio questo 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 il numero del repository viene inserito nella variabile count, quindi questo numero viene aggiunto all'array counts. Il problema con il codice è che fino all'arrivo dei dati del primo utente dal server, tutti gli utenti successivi saranno in modalità standby. Pertanto, viene elaborato un solo utente alla volta.

Se, ad esempio, per elaborare un utente occorrono circa 300 ms, allora per tutti gli utenti sono già un secondo; il tempo impiegato dipende linearmente dal numero di utenti. Ma poiché l'ottenimento del numero di pronti contro termine non dipende l'uno dall'altro, i processi possono essere parallelizzati. Ciò richiede l'utilizzo di .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 una serie di promesse come input e restituisce una promessa. Quest'ultimo, dopo che tutte le promesse dell'array sono state completate o al primo rifiuto, viene completato. Può succedere che non si avviino tutti contemporaneamente: per garantire un avvio simultaneo è possibile utilizzare p-map.

conclusione

Le funzioni asincrone stanno diventando sempre più importanti per lo sviluppo. Bene, per un uso adattivo delle funzioni asincrone, dovresti usare Iteratori asincroni. Uno sviluppatore JavaScript dovrebbe essere esperto in questo.

Skillbox consiglia:

Fonte: habr.com

Aggiungi un commento