Să ne uităm la Async/Await în JavaScript folosind exemple

Autorul articolului examinează exemple de Async/Await în JavaScript. În general, Async/Await este o modalitate convenabilă de a scrie cod asincron. Înainte de apariția acestei caracteristici, un astfel de cod a fost scris folosind apeluri înapoi și promisiuni. Autorul articolului original dezvăluie avantajele Async/Await analizând diverse exemple.

Amintim: pentru toți cititorii „Habr” - o reducere de 10 de ruble la înscrierea la orice curs Skillbox folosind codul promoțional „Habr”.

Skillbox recomandă: Curs educativ online „Dezvoltator Java”.

Callback

Callback este o funcție al cărei apel este întârziat pe termen nelimitat. Anterior, apelurile inverse erau folosite în acele zone de cod în care rezultatul nu putea fi obținut imediat.

Iată un exemplu de citire asincronă a unui fișier în Node.js:

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

Problemele apar atunci când trebuie să efectuați mai multe operații asincrone simultan. Să ne imaginăm acest scenariu: se face o solicitare către baza de date de utilizatori Arfat, trebuie să citiți câmpul profile_img_url al acestuia și să descărcați o imagine de pe serverul someserver.com.
După descărcare, convertim imaginea într-un alt format, de exemplu din PNG în JPEG. Dacă conversia a avut succes, se trimite o scrisoare către e-mailul utilizatorului. În continuare, informațiile despre eveniment sunt introduse în fișierul transformations.log, indicând data.

Merită să acordați atenție suprapunerii apelurilor inverse și numărului mare de }) în partea finală a codului. Se numește Callback Hell sau Pyramid of Doom.

Dezavantajele acestei metode sunt evidente:

  • Acest cod este greu de citit.
  • De asemenea, este dificil să gestionați erorile, ceea ce duce adesea la o calitate slabă a codului.

Pentru a rezolva această problemă, promisiunile au fost adăugate la JavaScript. Acestea vă permit să înlocuiți imbricarea profundă a apelurilor inverse cu cuvântul .then.

Aspectul pozitiv al promisiunilor este că fac codul mult mai ușor de citit, de sus în jos, mai degrabă decât de la stânga la dreapta. Cu toate acestea, promisiunile au și problemele lor:

  • Trebuie să adăugați o mulțime de .apoi.
  • În loc de try/catch, .catch este folosit pentru a gestiona toate erorile.
  • Lucrul cu mai multe promisiuni într-o singură buclă nu este întotdeauna convenabil; în unele cazuri, acestea complică codul.

Iată o problemă care va arăta semnificația ultimului punct.

Să presupunem că avem o buclă for care tipărește o secvență de numere de la 0 la 10 la intervale aleatorii (0–n secunde). Folosind promisiuni, trebuie să schimbați această buclă, astfel încât numerele să fie tipărite în secvență de la 0 la 10. Deci, dacă este nevoie de 6 secunde pentru a imprima un zero și 2 secunde pentru a imprima unul, zero ar trebui să fie imprimat mai întâi, apoi va începe numărătoarea inversă pentru tipărirea celei.

Și, desigur, nu folosim Async/Await sau .sort pentru a rezolva această problemă. Un exemplu de soluție este la sfârșit.

Funcții asincrone

Adăugarea de funcții asincrone în ES2017 (ES8) a simplificat sarcina de a lucra cu promisiuni. Observ că funcțiile asincrone funcționează „pe deasupra” promisiunilor. Aceste funcții nu reprezintă concepte diferite calitativ. Funcțiile asincrone sunt concepute ca o alternativă la codul care utilizează promisiuni.

Async/Await face posibilă organizarea muncii cu cod asincron într-un stil sincron.

Astfel, cunoașterea promisiunilor facilitează înțelegerea principiilor Async/Await.

sintaxă

În mod normal, acesta constă din două cuvinte cheie: async și await. Primul cuvânt transformă funcția în asincronă. Astfel de funcții permit utilizarea await. În orice alt caz, utilizarea acestei funcții va genera o eroare.

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

Async este inserat chiar la începutul declarației funcției, iar în cazul unei funcție săgeată, între semnul „=” și paranteze.

Aceste funcții pot fi plasate într-un obiect ca metode sau utilizate într-o declarație de clasă.

// 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! Merită să ne amintim că constructorii de clase și getters/setters nu pot fi asincroni.

Semantică și reguli de execuție

Funcțiile asincrone sunt în esență similare cu funcțiile JS standard, dar există și excepții.

Astfel, funcțiile asincrone returnează întotdeauna promisiuni:

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

Mai exact, fn returnează șirul salut. Ei bine, deoarece aceasta este o funcție asincronă, valoarea șirului este înfășurată într-o promisiune folosind un constructor.

Iată un design alternativ fără Async:

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

În acest caz, promisiunea este returnată „manual”. O funcție asincronă este întotdeauna cuprinsă într-o nouă promisiune.

Dacă valoarea returnată este o primitivă, funcția asincronă returnează valoarea prin includerea acesteia într-o promisiune. Dacă valoarea returnată este un obiect de promisiune, rezoluția sa este returnată într-o nouă promisiune.

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

Dar ce se întâmplă dacă există o eroare în interiorul unei funcții asincrone?

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

Dacă nu este procesat, foo() va returna o promisiune cu respingere. În această situație, Promise.reject care conține o eroare va fi returnat în loc de Promise.resolve.

Funcțiile asincrone produc întotdeauna o promisiune, indiferent de ceea ce este returnat.

Funcțiile asincrone se întrerup la fiecare așteptare.

Așteptați afectează expresiile. Deci, dacă expresia este o promisiune, funcția asincronă este suspendată până când promisiunea este îndeplinită. Dacă expresia nu este o promisiune, este convertită într-o promisiune prin intermediul Promise.resolve și apoi finalizată.

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

Și aici este o descriere a modului în care funcționează funcția fn.

  • După apelare, prima linie este convertită din const a = await 9; în const a = await Promise.resolve(9);.
  • După folosirea Await, execuția funcției este suspendată până când a își obține valoarea (în situația actuală este 9).
  • delayAndGetRandom(1000) întrerupe execuția funcției fn până când se completează (după 1 secundă). Acest lucru oprește efectiv funcția fn timp de 1 secundă.
  • delayAndGetRandom(1000) prin resolve returnează o valoare aleatorie, care este apoi atribuită variabilei b.
  • Ei bine, cazul cu variabila c este similar cu cazul cu variabila a. După aceea, totul se oprește pentru o secundă, dar acum delayAndGetRandom(1000) nu returnează nimic pentru că nu este necesar.
  • Ca rezultat, valorile sunt calculate folosind formula a + b * c. Rezultatul este împachetat într-o promisiune folosind Promise.resolve și returnat de funcție.

Aceste pauze ar putea să amintească de generatoarele din ES6, dar există ceva motivele tale.

Rezolvarea problemei

Ei bine, acum să ne uităm la soluția la problema menționată mai sus.

Funcția finishMyTask folosește Await pentru a aștepta rezultatele operațiunilor, cum ar fi queryDatabase, sendEmail, logTaskInFile și altele. Dacă comparați această soluție cu cea în care s-au folosit promisiuni, asemănările vor deveni evidente. Cu toate acestea, versiunea Async/Await simplifică foarte mult toate complexitățile sintactice. În acest caz, nu există un număr mare de apeluri inverse și lanțuri precum .then/.catch.

Iată o soluție cu ieșirea de numere, există două opțiuni.

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

Și iată o soluție care utilizează funcții asincrone.

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

Eroare la procesare

Erorile nerezolvate sunt incluse într-o promisiune respinsă. Cu toate acestea, funcțiile asincrone pot folosi try/catch pentru a gestiona erorile în mod sincron.

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() este o funcție asincronă care fie reușește („număr perfect”), fie eșuează cu o eroare („Ne pare rău, numărul este prea mare”).

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

Deoarece exemplul de mai sus se așteaptă ca canRejectOrReturn să se execute, propriul său eșec va duce la executarea blocului catch. Ca urmare, funcția foo se va încheia fie cu nedefinit (când nimic nu este returnat în blocul try), fie cu o eroare prinsă. Ca rezultat, această funcție nu va eșua deoarece try/catch va gestiona funcția foo în sine.

Iată un alt exemplu:

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

Merită să acordați atenție faptului că, în exemplu, canRejectOrReturn este returnat de la foo. Foo în acest caz fie se termină cu un număr perfect, fie returnează o eroare („Ne pare rău, numărul este prea mare”). Blocul catch nu va fi niciodată executat.

Problema este că foo returnează promisiunea transmisă de la canRejectOrReturn. Deci soluția pentru foo devine soluția pentru canRejectOrReturn. În acest caz, codul va fi format din doar două linii:

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

Iată ce se întâmplă dacă folosiți așteptați și reveniți împreună:

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

În codul de mai sus, foo va ieși cu succes atât cu un număr perfect, cât și cu o eroare prinsă. Aici nu vor fi refuzuri. Dar foo va reveni cu canRejectOrReturn, nu cu undefined. Să ne asigurăm de acest lucru eliminând linia return await canRejectOrReturn():

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

Greșeli și capcane comune

În unele cazuri, utilizarea Async/Await poate duce la erori.

Așteaptă uitată

Acest lucru se întâmplă destul de des - cuvântul cheie await este uitat înainte de promisiune:

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

După cum puteți vedea, nu există nicio așteptare sau întoarcere în cod. Prin urmare, foo iese întotdeauna cu undefined fără o întârziere de 1 secundă. Dar promisiunea se va împlini. Dacă aruncă o eroare sau o respingere, atunci va fi apelat UnhandledPromiseRejectionWarning.

Funcții asincrone în apeluri inverse

Funcțiile asincrone sunt destul de des folosite în .map sau .filter ca apeluri inverse. Un exemplu este funcția fetchPublicReposCount(nume utilizator), care returnează numărul de depozite deschise pe GitHub. Să presupunem că există trei utilizatori de ale căror valori avem nevoie. Iată codul pentru această sarcină:

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'];
}

Avem nevoie de conturi ArfatSalman, octocat, norvig. În acest caz facem:

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

Merită să acordați atenție Await în apelarea .map. Aici counts este o serie de promisiuni, iar .map este un apel invers anonim pentru fiecare utilizator specificat.

Utilizarea excesiv de consecventă a așteaptă

Să luăm acest cod ca exemplu:

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

Aici numărul repo este plasat în variabila count, apoi acest număr este adăugat la tabloul counts. Problema cu codul este că până când datele primului utilizator vor ajunge de pe server, toți utilizatorii următori vor fi în modul de așteptare. Astfel, un singur utilizator este procesat la un moment dat.

Dacă, de exemplu, este nevoie de aproximativ 300 ms pentru a procesa un utilizator, atunci pentru toți utilizatorii este deja o secundă; timpul petrecut depinde liniar de numărul de utilizatori. Dar, deoarece obținerea numărului de repo nu depinde una de alta, procesele pot fi paralelizate. Acest lucru necesită lucrul cu .map și 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 primește o serie de promisiuni ca intrare și returnează o promisiune. Acesta din urmă, după ce toate promisiunile din matrice s-au încheiat sau la prima respingere, este finalizat. Se poate întâmpla ca toate să nu pornească în același timp - pentru a asigura pornirea simultană, puteți utiliza p-map.

Concluzie

Funcțiile asincrone devin din ce în ce mai importante pentru dezvoltare. Ei bine, pentru utilizarea adaptivă a funcțiilor asincrone, ar trebui să utilizați Iteratoare asincrone. Un dezvoltator JavaScript ar trebui să cunoască bine acest lucru.

Skillbox recomandă:

Sursa: www.habr.com

Adauga un comentariu