Izpētām Async/Await JavaScript ar piemēriem

Raksta autors izskaidro ar piemēriem Async/Await JavaScript. Kopumā, Async/Await ir ērta pieeja asinsynchronous koda rakstīšanai. Pirms šīs iespējas šādu kodu rakstīja, izmantojot atgriezeniskās saites un solījumus. Oriģinālā raksta autors atklāj Async/Await priekšrocības, analizējot dažādus piemērus.

Atgādinām: visiem «Habr» lasītājiem — atlaide 10 000 rubļu reģistrējoties jebkurā Skillbox kursā ar promo kodu «Habr».

Skillbox iesaka: Izglītības tiešsaistes kurss Java izstrādātājs.

Atgriezeniskā saite

Atgriezeniskā saite ir funkcija, kuras izsaukšana tiek atlikta uz nenoteiktu laiku. Agrāk atgriezeniskās saites tika izmantotas to koda daļu vietās, kur rezultāts nevarēja tikt iegūts uzreiz.

Šeit ir piemērs asynhrona faila lasīšanai Node.js:

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

Problēmas rodas brīdī, kad nepieciešams veikt vairākas asinsynchronous operācijas uzreiz. Iedomāsimies šādu scenāriju: tiek veikts pieprasījums uz datu bāzi par lietotāju Arfat, ir jāizlasa viņa lauks profile_img_url un jāsagaida attēls no servera someserver.com.
Pēc attēla augšupielādes mēs to konvertējam citā formātā, piemēram, no PNG uz JPEG. Ja konversija ir veiksmīga, lietotājam tiek nosūtīta e-pasta vēstule. Pēc tam informācija par notikumu tiek ierakstīta failā transformations.log, norādot datumu.

Ir vērts pievērst uzmanību atgriezenisko zvanu pārslogotībai un lielajam skaitam }) gala daļā kodā. To sauc par Callback Hell vai Pyramid of Doom.

Šāda paņēmiņa trūkumi ir acīmredzami:

  • Šo kodu ir grūti lasīt.
  • Tajā arī ir grūti apstrādāt kļūdas, kas bieži noved pie kodeļa kvalitātes pasliktināšanās.

Lai risinātu šo problēmu, JavaScript tika pievienoti solījumi. Tie ļauj nomainīt dziļo atgriezeniskās saites iegremdēšanu uz vārdu .then.

Pozitīva solījumu puse ir tāda, ka ar tiem kodu ir daudz vieglāk lasīt, turklāt no augšas uz leju, nevis no kreisās uz labo. Tomēr solījumiem arī ir savas problēmas:

  • Jāpieņem liels skaits .then.
  • Vietā try/catch tiek izmantots .catch, lai apstrādātu visas kļūdas.
  • Darbs ar vairākiem solījumiem vienā ciklā ne vienmēr ir ērts, dažos gadījumos tie sarežģī kodu.

Šeit ir uzdevums, kas parādīs pēdējā punkta nozīmi.

Pieņemsim, ka ir for cikls, kas izvada skaitļus no 0 līdz 10 ar nejaušu intervālu (0–n sekundes). Izmantojot solījumus (promises), šis cikls jāmaina tā, lai skaitļi tiktu izvadīti secībā no 0 līdz 10. Tātad, ja nulles izvadīšana aizņem 6 sekundes, bet vieninieka — 2 sekundes, vispirms jāizvada nulle, un tikai tad var sākties vieninieka izvadīšana.

Un protams, šīs problēmas risināšanai mēs neizmantojam Async/Await vai .sort. Risinājuma piemērs būs beigās.

Async funkcijas

Async funkciju pievienošana ES2017 (ES8) atviegloja darbu ar solījumiem. Jāatzīmē, ka async funkcijas darbojas "virs" solījumiem. Šīs funkcijas nav kvalitatīvi citi koncepti. Async funkcijas tika domātas kā alternatīva kodam, kas izmanto solījumus.

Async/Await sniedz iespēju organizēt darbu ar asinhrono kodu sinhronā stilā.

Tādējādi solījumu zināšanas atvieglo Async/Await principu izpratni.

Sintakse

Parastā situācijā tas sastāv no diviem atslēgvārdiem: async un await. Pirmais atslēgvārds pārvērš funkciju par asinhronu. Šādās funkcijās ir atļauts izmantot await. Jebkurā citā gadījumā šīs funkcijas izmantošana izraisīs kļūdu.

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

Async tiek ievietots funkcijas deklarēšanas sākumā, bet, izmantojot bultiņu funkciju, starp zīmi "=" un iekavām.

Šīs funkcijas var ievietot objektā kā metodes vai izmantot klases deklarācijā.

// 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! Jāatceras, ka klases konstruktori un getteri/setteri nevar būt asinhroni.

Semantika un izpildes noteikumi

Async funkcijas principā ir līdzīgas standarta JS funkcijām, taču ir arī izņēmumi.

Tātad, async funkcijas vienmēr atgriež solījumus:

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

Īpaši, fn atgriež virkni hello. Tā kā tā ir asinhrona funkcija, šīs virknes vērtība tiek iesaiņota solījumā ar konstruktora palīdzību.

Šeit ir alternatīva konstrukcija bez Async:

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

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

Šajā gadījumā solījuma atgriešana tiek veikta "roku darbs". Asinhrona funkcija vienmēr tiek iesaiņota jaunā solījumā.

Ja gadījumā, ja atgrieztais vērtība ir primitīvs, async-funkcija veic atgriešanu, iesaiņojot to solījumā. Ja atgrieztais vērtība ir solījuma objekts, tā risinājums tiek atgriezts jaunā solījumā.

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

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

Bet kas notiks, ja iekšpusē asinkronās funkcijas notiks kļūda?

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

foo().catch(console.log);

Ja tā netiks apstrādāta, foo() atgriezīs solījumu ar atteikumu. Šajā situācijā vietā Promise.resolve tiks atgriezts Promise.reject, kurā ir kļūda.

Async-funkcijas vienmēr atgriež solījumu, neatkarīgi no tā, kas tiek atgriezts.

Asinkronās funkcijas apstājas pie katra await.

Await ietekmē izteiksmes. Tādējādi, ja izteiksme ir solījums, async-funkcija apstājas līdz solījuma izpildei. Gadījumā, ja izteiksme nav solījums, tā tiek konvertēta par solījumu caur Promise.resolve un pēc tam tiek pabeigta.

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

Te ir apraksts par to, kā darbojas fn-funkcija.

  • Pēc tās izsaukuma pirmā rinda tiek konvertēta no const a = await 9; uz const a = await Promise.resolve(9);.
  • Pēc Await izmantošanas funkcijas izpilde tiek apturēta, līdz a saņem savu vērtību (šajā situācijā tas ir 9).
  • delayAndGetRandom(1000) aptur fn-funkcijas izpildi, kamēr tā nepabeidz (pēc 1 sekundes). Tas faktiski ir fn-funkcijas apstāšanās uz 1 sekundi.
  • delayAndGetRandom(1000) caur resolve atgriež nejaušu vērtību, kas tad tiek piešķirta mainīgajam b.
  • Bet gadījums ar mainīgo c ir līdzīgs gadījumam ar mainīgo a. Pēc tam viss apstājas uz sekundi, bet tagad delayAndGetRandom(1000) nekas netiek atgriezts, jo tas nav nepieciešams.
  • Rezultātā vērtības tiek aprēķinātas ar formulu a + b * c. Rezultāts tiek iesaiņots solījumā, izmantojot Promise.resolve, un tiek atgriezts no funkcijas.

Šie pārrāvumi var atgādināt ģeneratorus ES6, bet tam ir savējie iemesli.

Risinām uzdevumu

Tagad aplūkosim iepriekš minētā uzdevuma risinājumu.

Function finishMyTask uses Await to wait for the results of operations such as queryDatabase, sendEmail, logTaskInFile, and others. When comparing this solution to one using promises, the similarities become clear. However, the version with Async/Await significantly simplifies all syntactic complexities. In this case, there aren't many callbacks and chains like .then/.catch.

Here’s a solution to output numbers, with two variants available.

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

And here’s a solution using async functions.

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

Error handling

Unhandled errors are wrapped in a rejected promise. However, in async functions, the try/catch construct can be used for synchronous error handling.

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() is an asynchronous function that either successfully completes (“perfect number”) or fails with an error (“Sorry, number too big”).

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

Since canRejectOrReturn is awaited in the above example, an unsuccessful completion will trigger the catch block. As a result, the foo function will finish either with undefined (when nothing is returned in the try block) or with error caught. Thus, this function will not have a failure since the try/catch will handle foo itself.

Here’s another example:

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

It’s important to note that in the example from foo, canRejectOrReturn is returned. Foo in this case completes either with perfect number or an Error (“Sorry, number too big”). The catch block will never execute.

The problem is that foo returns a promise from canRejectOrReturn. Therefore, the solution of the foo function becomes the solution for canRejectOrReturn. In this case, the code will consist of just two lines:

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

And here’s what happens if you use await and return together:

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

Iepriekš minētajā kodā foo veiksmīgi pabeigsies gan ar perfect number, gan ar error caught. Šeit atteikumu nebūs. Tomēr foo pabeigsies ar canRejectOrReturn, nevis ar undefined. Pārliecināsimies par to, izņemot rindu return await canRejectOrReturn():

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

Izplatītas kļūdas un slēptās bedres

Dažos gadījumos Async/Await izmantošana var radīt kļūdas.

Aizmirsts await

Tas notiek diezgan bieži — pirms solījuma tiek aizmirsts atslēgvārds await:

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

Kods, kā redzams, nesatur ne await, ne return. Tāpēc foo vienmēr pabeidzas ar undefined bez 1 sekundes kavēšanās. Tomēr solījums tiks izpildīts. Ja tas tomēr izsniegs kļūdu vai tiks atteikts, šajā gadījumā tiks izsaukts UnhandledPromiseRejectionWarning.

Async funkcijas atgriezienu saucienos

Async funkcijas diezgan bieži izmanto .map vai .filter kā atgriezienu saucienos. Piemēram, var minēt funkciju fetchPublicReposCount(username), kas atgriež atvērtā GitHub repozitoriju skaitu. Pieņemam, ka mums ir trīs lietotāji, kuru rādītājus mēs vēlamies iegūt. Šeit ir kods šai uzdevumam:

const url = 'https://api.github.com/users';
 
// Palīgfunkcija, lai iegūtu repozitoriju skaitu
const fetchPublicReposCount = async (username) => {
  const response = await fetch(`${url}/${username}`);
  const json = await response.json();
  return json['public_repos'];
}

Mums nepieciešami konti ArfatSalman, octocat, norvig. Šajā gadījumā veicam:

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

Jāņem vērā Await atgriezienu saucienā .map. Šeit counts ir solījumu masīvs, un .map ir anonīms atgriezienu sauciens katram norādītajam lietotājam.

Pārmērīga sekvencēla await izmantošana

Piemēram, apskatīsim šādu kodu:

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

Šeit mainīgajā count tiek ievietots repozitoriju skaits, pēc tam šis skaitlis tiek pievienots masīvam counts. Koda problēma ir tāda, ka, kamēr no servera nenāk datu pirmā lietotāja, visi pārējie lietotāji gaida. Tādējādi vienlaikus tiek apstrādāts tikai viens lietotājs.

Ja, piemēram, ja viena lietotāja apstrādei nepieciešamas apmēram 300 ms, tad visiem lietotājiem tas jau ir sekunde, izmaksātā laika atkarība lineāri mainās, atkarībā no lietotāju skaita. Bet, ņemot vērā, ka repo skaita iegūšana nav atkarīga viena no otras, procesus var paralelizēt. Tam nepieciešama .map un Promise.all izmantošana:

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

Promise.all saņem masīvu ar solījumiem un atgriež solījumu. Tas beidzas pēc visu solījumu pabeigšanas masīvā vai pie pirmā atteikuma. Var gadīties, ka tie visi nenonāk uzsākšanās laikā, — lai nodrošinātu vienlaicīgu palaišanu, var izmantot p-map.

Secinājums

Async funkcijas kļūst arvien svarīgākas izstrādē. Bet adaptīvai async funkciju izmantošanai ir vērts izmantot Async Iterators. JavaScript izstrādātājam jābūt labi pārzinošam sulū.

Skillbox iesaka:

Avots: habr.com

Iegādājieties uzticamu tīmekļa hostingu ar DDoS aizsardzību, VPS VDS serveriem 🔥 Iegādājieties uzticamu tīmekļa hostingu ar DDoS aizsardzību, VPS VDS serveriem | ProHoster