Vaatame näidete abil JavaScripti asünkrooni/ootmist

Artikli autor uurib Async/Awaiti näiteid JavaScriptis. Üldiselt on Async/Await mugav viis asünkroonse koodi kirjutamiseks. Enne selle funktsiooni ilmumist kirjutati selline kood tagasihelistamiste ja lubaduste abil. Algse artikli autor avab Async/Awaiti eeliseid erinevaid näiteid analüüsides.

Tuletame meelde: kõigile "Habr" lugejatele - allahindlus 10 000 rubla, kui registreerute mis tahes Skillboxi kursusele, kasutades sooduskoodi "Habr".

Skillbox soovitab: Hariv veebikursus "Java arendaja".

Callback

Tagasihelistamine on funktsioon, mille kõne hilineb määramata ajaks. Varem kasutati tagasihelistamist nendes koodipiirkondades, kus tulemust kohe ei saanud.

Siin on näide faili asünkroonsest lugemisest Node.js-s:

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

Probleemid tekivad siis, kui on vaja teha mitu asünkroonset toimingut korraga. Kujutagem ette sellist stsenaariumi: Arfati kasutajate andmebaasi tehakse päring, peate lugema selle välja profile_img_url ja laadima mõne serverist someserver.com pildi alla.
Pärast allalaadimist teisendame pildi muusse vormingusse, näiteks PNG-st JPEG-vormingusse. Kui teisendamine õnnestus, saadetakse kasutaja meilile kiri. Järgmisena sisestatakse sündmuse teave faili transformations.log, märkides kuupäeva.

Tähelepanu tasub pöörata tagasihelistamiste kattuvusele ja suurele arvule }) koodi viimases osas. Seda nimetatakse tagasihelistamispõrguks või hukatuse püramiidiks.

Selle meetodi puudused on ilmsed:

  • Seda koodi on raske lugeda.
  • Samuti on raske toime tulla vigadega, mis sageli põhjustab halva koodikvaliteedi.

Selle probleemi lahendamiseks lisati JavaScriptile lubadused. Need võimaldavad asendada tagasihelistamiste sügava pesastuse sõnaga .then.

Lubaduste positiivne külg on see, et need muudavad koodi palju paremini loetavaks, pigem ülalt alla kui vasakult paremale. Kuid lubadustel on ka oma probleemid:

  • Peate lisama palju .siis.
  • Try/catch asemel kasutatakse kõigi vigade käsitlemiseks .catch.
  • Ühes tsüklis mitme lubadusega töötamine ei ole alati mugav; mõnel juhul muudavad need koodi keerulisemaks.

Siin on probleem, mis näitab viimase punkti tähendust.

Oletame, et meil on for-silmus, mis prindib juhuslike ajavahemike järel (0–n sekundit) numbrijada vahemikus 10 kuni 0. Lubadusi kasutades peate seda tsüklit muutma nii, et numbrid trükitakse järjestikku vahemikus 0 kuni 10. Seega, kui nulli printimiseks kulub 6 sekundit ja ühe printimiseks 2 sekundit, tuleks kõigepealt trükkida null ja seejärel algab selle printimise loendur.

Ja loomulikult ei kasuta me selle probleemi lahendamiseks faili Async/Await või .sort. Näidislahendus on lõpus.

Asünkroniseerimisfunktsioonid

Asünkroonimisfunktsioonide lisamine ES2017-sse (ES8) lihtsustas lubadustega töötamist. Märgin, et asünkroonimisfunktsioonid töötavad lubaduste peal. Need funktsioonid ei esinda kvalitatiivselt erinevaid mõisteid. Asünkroonimisfunktsioonid on mõeldud lubadusi kasutava koodi alternatiiviks.

Async/Await võimaldab korraldada tööd asünkroonse koodiga sünkroonses stiilis.

Seega on lubaduste teadmine lihtsam mõista Async/Awaiti põhimõtteid.

süntaks

Tavaliselt koosneb see kahest märksõnast: asünkroonimine ja ootamine. Esimene sõna muudab funktsiooni asünkroonseks. Sellised funktsioonid võimaldavad kasutada ootamist. Igal muul juhul tekitab selle funktsiooni kasutamine vea.

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

Async sisestatakse funktsiooni deklaratsiooni algusesse ja noolefunktsiooni korral märgi “=” ja sulgude vahele.

Neid funktsioone saab paigutada objektidesse meetoditena või kasutada klassideklaratsioonis.

// 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! Tasub meeles pidada, et klassikonstruktorid ja getterid/seadistajad ei saa olla asünkroonsed.

Semantika ja täitmise reeglid

Asünkroonfunktsioonid on põhimõtteliselt sarnased tavaliste JS-funktsioonidega, kuid on ka erandeid.

Seega tagastavad asünkroonimisfunktsioonid alati lubadused:

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

Täpsemalt, fn tagastab stringi tere. Noh, kuna see on asünkroonne funktsioon, mähitakse stringi väärtus konstruktori abil lubadusse.

Siin on alternatiivne disain ilma asünkroonita:

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

Sel juhul tagastatakse lubadus "käsitsi". Asünkroonne funktsioon on alati mähitud uude lubadusse.

Kui tagastatav väärtus on primitiivne, tagastab asünkroonimisfunktsioon väärtuse, mähkides selle lubadusse. Kui tagastatav väärtus on lubaduse objekt, tagastatakse selle resolutsioon uues lubaduses.

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

Aga mis juhtub, kui asünkroonses funktsioonis on viga?

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

Kui seda ei töödelda, tagastab foo() lubaduse tagasilükkamisega. Sellises olukorras tagastatakse Promise.resolve asemel viga sisaldav Promise.reject.

Asünkroonfunktsioonid väljastavad alati lubaduse, olenemata tagastatavast.

Asünkroonsed funktsioonid peatuvad igal ootel.

Ootama mõjutab väljendeid. Seega, kui avaldis on lubadus, peatatakse asünkroonimisfunktsioon, kuni lubadus on täidetud. Kui avaldis ei ole lubadus, teisendatakse see lubaduseks läbi Promise.resolve ja seejärel täidetakse.

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

Ja siin on kirjeldus, kuidas funktsioon fn töötab.

  • Pärast selle väljakutsumist teisendatakse esimene rida väärtusest const a = await 9; in const a = ootama lubadust.resolve(9);.
  • Pärast Oota kasutamist peatatakse funktsiooni täitmine, kuni a saab oma väärtuse (praeguses olukorras on see 9).
  • delayAndGetRandom(1000) peatab fn-funktsiooni täitmise, kuni see ise lõpetab (1 sekundi pärast). See peatab tõhusalt fn-funktsiooni 1 sekundiks.
  • delayAndGetRandom(1000) lahendamise kaudu tagastab juhusliku väärtuse, mis määratakse seejärel muutujale b.
  • Noh, muutuja c juhtum on sarnane muutuja a juhtumiga. Pärast seda peatub kõik hetkeks, kuid nüüd delayAndGetRandom(1000) ei tagasta midagi, sest seda ei nõuta.
  • Selle tulemusena arvutatakse väärtused valemiga a + b * c. Tulemus mähitakse lubadusesse kasutades Promise.resolve ja tagastatakse funktsiooni poolt.

Need pausid võivad meenutada ES6 generaatoreid, kuid selles on midagi teie põhjused.

Probleemi lahendamine

Vaatame nüüd ülalmainitud probleemi lahendust.

Funktsioon finishMyTask kasutab funktsiooni Await, et oodata selliste toimingute tulemusi nagu queryDatabase, sendEmail, logTaskInFile ja teised. Kui võrrelda seda lahendust sellega, kus lubadusi kasutati, ilmnevad sarnasused. Kuid Async/Await versioon lihtsustab oluliselt kõiki süntaktilisi keerukusi. Sel juhul pole suurt hulka tagasihelistamisi ja ahelaid nagu .then/.catch.

Siin on lahendus numbrite väljundiga, valikuid on kaks.

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

Ja siin on lahendus, mis kasutab asünkroonimisfunktsioone.

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

Viga töötlemisel

Käsitlemata vead on mähitud tagasilükatud lubadusse. Kuid asünkroonimisfunktsioonid võivad vigade sünkroonseks käsitlemiseks kasutada proovi/saaki.

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() on asünkroonne funktsioon, mis kas õnnestub ("täiuslik arv") või ebaõnnestub veaga ("Vabandust, number on liiga suur").

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

Kuna ülaltoodud näide eeldab, et canRejectOrReturn käivitub, põhjustab selle enda tõrge püüdmisploki täitmise. Selle tulemusena lõpeb funktsioon foo kas määramata (kui prooviplokis ei tagastata midagi) või tabatud veaga. Selle tulemusena see funktsioon ei ebaõnnestu, sest try/catch tegeleb funktsiooniga foo ise.

Siin on veel üks näide:

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

Tähelepanu tasub pöörata asjaolule, et näites on foo tagastatud canRejectOrReturn. Foo lõpetab sel juhul täiusliku numbri või tagastab vea ("Vabandust, number on liiga suur"). Püügiplokki ei käivitata kunagi.

Probleem on selles, et foo tagastab canRejectOrReturnilt antud lubaduse. Seega saab lahendusest foo lahenduseks canRejectOrReturn. Sel juhul koosneb kood ainult kahest reast:

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

Kui kasutate oote- ja naasmisfunktsiooni koos:

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

Ülaltoodud koodis väljub foo edukalt nii täiusliku numbri kui ka tabatud veaga. Siin ei tehta keeldumisi. Kuid foo naaseb käsuga canRejectOrReturn, mitte aga undefinediga. Veendume selles, eemaldades rea return await canRejectOrReturn():

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

Levinud vead ja lõksud

Mõnel juhul võib Async/Awaiti kasutamine põhjustada vigu.

Unustatud ootama

Seda juhtub üsna sageli - ootamise märksõna unustatakse enne lubadust:

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

Nagu näete, ei ole koodis ootamist ega tagastamist. Seetõttu väljub foo alati undefined'iga ilma 1-sekundilise viivituseta. Aga lubadus saab täidetud. Kui see annab vea või tagasilükkamise, kutsutakse välja UnhandledPromiseRejectionWarning.

Asünkroonimisfunktsioonid tagasihelistamisel

Asünkroonfunktsioone kasutatakse .map või .filter puhul üsna sageli tagasihelistamistena. Näiteks on funktsioon fetchPublicReposCount(username), mis tagastab GitHubi avatud hoidlate arvu. Oletame, et meil on kolm kasutajat, kelle mõõdikuid vajame. Siin on selle ülesande kood:

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

Vajame ArfatSalmani, octocati, norvigi kontosid. Sel juhul teeme:

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

Tasub pöörata tähelepanu .map-i tagasihelistamisele Oota. Siin loeb rida lubadusi ja .map on iga määratud kasutaja anonüümne tagasihelistamine.

Oota liiga järjekindel kasutamine

Võtame selle koodi näitena:

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

Siin paigutatakse repo number loendusmuutujasse, seejärel lisatakse see arv loenduste massiivi. Koodi probleem seisneb selles, et kuni esimese kasutaja andmete saabumiseni serverist on kõik järgnevad kasutajad ooterežiimis. Seega töödeldakse korraga ainult ühte kasutajat.

Kui näiteks ühe kasutaja töötlemiseks kulub umbes 300 ms, siis kõikidel kasutajatel on see juba sekund, ajakulu sõltub lineaarselt kasutajate arvust. Kuid kuna repode arvu saamine ei sõltu üksteisest, saab protsesse paralleelselt teha. See nõuab tööd .map ja Promise.all-iga:

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

Promise.all saab sisendiks hulga lubadusi ja tagastab lubaduse. Viimane täidetakse pärast kõigi massiivi lubaduste täitmist või esmakordsel tagasilükkamisel. Võib juhtuda, et need kõik ei käivitu samal ajal – samaaegse stardi tagamiseks saab kasutada p-kaarti.

Järeldus

Asünkroonfunktsioonid muutuvad arenduse jaoks üha olulisemaks. Noh, asünkroonimisfunktsioonide adaptiivseks kasutamiseks peaksite kasutama Asünkroonimise iteraatorid. JavaScripti arendaja peaks selles hästi kursis olema.

Skillbox soovitab:

Allikas: www.habr.com

Lisa kommentaar