Vaatame Async/Await JavaScriptis näidete kaudu

Artikli autor toob välja Async/Await näited JavaScriptis. Üldiselt on Async/Await mugav viis asünkroonse koodi kirjutamiseks. Enne selle võimaluse ilmumist kirjutati sellist koodi tagasiside ja lubaduste abil. Algse artikli autor tutvustab Async/Await eeliseid, tuues esile mitmeid näiteid.

Tuletame meelde: kõigile «Habra» lugejatele — 10 000 rubla soodustus, kui registreerite end Skillboxi mis tahes kursusele promokoodi «Habr» abil.

Skillbox soovitab: Hariduslik veebikursus «Java-arendaja».

Tagasiside

Tagasiside on funktsioon, mille kutsumine lükatakse määramata ajaks edasi. Varem kasutati tagasisideid koodilõikudes, kus tulemust ei saanud kohe saada.

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

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

Probleemid tekivad siis, kui tuleb korraga teostada mitu asünkroonset operatsiooni. Kujutame ette järgmist stsenaariumi: võtame andmebaasist kasutaja Arfat andmeid, peame lugema tema profile_img_url välja ja laadima pildi serverist someserver.com.
Pärast üleslaadimist konverteerime pildi teise formaati, näiteks PNG-st JPEG-ks. Kui konverteerimine õnnestub, saadetakse kasutaja e-posti aadressile kiri. Edasi kantakse sündmuse teave transformations.log faili koos kuupäeva märkimisega.

Oluline on tähele panna tagasivõtmiste ja suure arvu }) koondumist koodi lõpus. Seda tuntakse kui Callback Hell või Pyramid of Doom.

Selle meetodi puudused on ilmne:

  • Seda koodi on raske lugeda.
  • Sellega on samuti keeruline vigu hallata, mis sageli viib koodi kvaliteedi halvenemiseni.

Selle probleemi lahendamiseks on JavaScripti lisatud lubadused. Need võimaldavad asendada sügava kollaažimise .then-iga.

Lubaduste positiivne külg on see, et nendega on kood palju paremini loetav, ning see kulgeb ülespoole, mitte vasakult paremale. Siiski on lubadustel ka oma probleemid:

  • Tuleb lisada palju .then.
  • Try/catch'i asemel kasutatakse .catch'i kõigi vigade käsitlemiseks.
  • Mitme lubadusega töötamine ühe tsükli raames pole kaugeltki alati mugav, mõnes olukorras muudavad nad koodi keerulisemaks.

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

Kujutame ette for-tsüklit, mis väljastab numbrite järjekorra vahemikus 0 kuni 10 juhuslike intervallidega (0–n sekundit). Kasutades lubadusi, tuleb see tsükkel muuta nii, et numbrid väljastatakse järjestikku 0-st 10-ni. Näiteks, kui nulli väljastamine kestab 6 sekundit ja ühele 2 sekundit, siis peaks esmalt olema väljastatud null, enne kui ühe väljastamine algab.

Ja loomulikult ei kasuta me selle ülesande lahendamiseks Async/Await ega .sort. Näide lahendusest on lõpus.

Async-funktsioonid

Async-funktsioonide lisamine ES2017 (ES8) lihtsustas lubadustega töötamise ülesannet. Tahan rõhutada, et async-funktsioonid töötavad "lubaduste" peal. Need funktsioonid ei esinda kvaliteetselt teistsuguseid kontseptsioone. Async-funktsioonid mõeldi välja alternatiivina koodile, mis kasutab lubadusi.

Async/Await annab võimaluse korraldada asünkroonse koodi töötlemist sünkroonse stiili järgi.

Seega, lubaduste tundmine lihtsustab Async/Await põhimõtete mõistmist.

Süntaks

Tavaliselt koosneb see kahest võtmesõnast: async ja await. Esimene sõna muudab funktsiooni asünkroonseks. Sellistes funktsioonides on await'i kasutamine lubatud. Igas muus olukorras nende funktsioonide kasutamine tekitab 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 lisatakse kõigepealt funktsiooni deklareeringusse ja kui kasutatakse noolfunktsiooni, siis '=' ja sulgude vahele.

Need funktsioonid saab paigutada objekti meetoditena või kasutada klassi deklareerimisel.

// 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! Tuleb meeles pidada, et klasside konstruktorid ja getter'id/setter'id ei saa olla asünkoolsed.

Semantika ja täitmisreeglid

Async-funktsioonid on põhimõtteliselt sarnased tavalistele JS-funktsioonidele, kuid erandeid on.

Nii et async-funktsioonid tagastavad alati lubadusi:

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

Konkreetsemalt tagastab fn stringi hello. Kuna see on asünkroonne funktsioon, siis see stringi väärtus mähitakse lubadusse konstruktori abil.

Siin on alternatiivne konstruktsioon ilma Async-ta:

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

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

Sellisel juhul toimub lubaduse tagastamine „kätega“. Asünkroonset funktsiooni mähitakse alati uude lubadusse.

Kui tagastatav väärtus on primitiivne, siis async-funktsioon tagastab väärtuse, ümber pakkides selle lubadusse. Kui tagastatav väärtus on lubaduse objekt, siis selle lahendus tagastatakse uues lubaduses.

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

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

Kuid mis juhtub, kui async-funktsiooni sees tekib viga?

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

foo().catch(console.log);

Kui viga ei käsitleta, siis foo() tagastab lubaduse, millel on reject. Sel juhul tagastatakse Promise.resolve asemel Promise.reject koos veaga.

Async-funktsioonid tagastavad alati lubaduse, olenemata sellest, mis tagastatakse.

Asünkroonsed funktsioonid peatuvad iga await'i korral.

Await mõjutab väljendeid. Kui väljend on lubadus, peatub async-funktsioon lubaduse täitmiseni. Kui väljend ei ole lubadus, konverteeritakse see lubaduseks läbi Promise.resolve ja seejärel lõpetatakse.

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

Siin on kirjeldus sellest, kuidas fn-funktsioon töötab.

  • Pärast selle väljakutset konverteeritakse esimene rida const a = await 9; argumendiks const a = await Promise.resolve(9);.
  • Pärast Await kasutamist peatub funktsiooni täitmine, kuni see ei saa oma väärtust (käesoleval juhul 9).
  • delayAndGetRandom(1000) peatab fn-funktsiooni täitmise, kuni see ise lõpetatakse (pärast 1 sekundit). See on sisuliselt fn-funktsiooni peatamine 1 sekundiks.
  • delayAndGetRandom(1000) tagastab resolve'i kaudu juhusliku väärtuse, mis seejärel antakse muutujale b.
  • Ja muutuja c juhtum on sarnane muutuja a juhtumile. Pärast seda peatuvad kõik sekundiks, kuid nüüd delayAndGetRandom(1000) ei tagasta midagi, kuna see pole vajalik.
  • Seega arvutatakse väärtused valemi järgi a + b * c. Tulemuseks on lubaduse sisestamine Promise.resolve abil ja selle tagastamine funktsiooni kaudu.

Need pausid võivad meenutada loodreid ES6-s, kuid sellele on oma põhjused..

Lahendame ülesande.

Nüüd vaatame lahendust ülesandele, mis oli eespool mainitud.

Funktsioonis finishMyTask kasutatakse Await, et oodata selliste operatsioonide nagu queryDatabase, sendEmail, logTaskInFile tulemusi. Kui võrrelda seda lahendust lubadega, on sarnasused ilmsed. Siiski, Async/Await versioon lihtsustab oluliselt kõiki süntaktilisi keerukusi. Sellisel juhul pole palju tagasiside funktsioone ja ahelaid nagu .then/.catch.

Siin on lahendus numbrite väljastamiseks, kus on kaks 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);
    });
  });
};

Ja siin on lahendus, mis kasutab async-funktsioone.

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

Vigade käsitsemine

Töötlemata vead ümbritsetakse tagasi lükatud lubadusse. Siiski saavad async-funktsioonides kasutada try/catch konstruktsiooni, et teostada sünkroonne veahaldus.

async function canRejectOrReturn() {
  // oota üks sekund
  await new Promise(res => setTimeout(res, 1000));

  // tagasi lükata ~50% tõenäosusega
  if (Math.random() > 0.5) {
    throw new Error('Kahjuks on number liiga suur.')
  }

  return 'täiuslik number';
}

canRejectOrReturn() on asünkroonne funktsioon, mis kas täidetakse edukalt ("täiuslik number") või see lõppeb veaga ("Kahjuks on number liiga suur").

async function foo() {
  try {
    await canRejectOrReturn();
  } catch (e) {
    return 'viga püüti';
  }
}

Kuna ülaltoodud näites oodatakse canRejectOrReturn'i täitmist, siis oma ebaõnnestumine toob kaasa catch-bloki täitmise. Selle tulemusena lõpeb funktsioon foo kas undefined'iga (kui try-blokis midagi ei tagastata) või error caught'iga. Selle tulemusena ei saa selle funktsiooni lõppeda ebaõnnestumisega, kuna try/catch tegeleb ise funktsiooni foo töötlemisega.

Siin on veel üks näide:

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

Oluline on tähelepanu pöörata sellele, et foo näites tagastatakse canRejectOrReturn. Foo lõpeb sel juhul kas perfect number'iga või tagastab vea Error (“Kahjuks, number liiga suur”). Catch-blokki kunagi ei täideta.

Probleem on selles, et foo tagastab lubaduse, mis on edastatud canRejectOrReturn'ilt. Seetõttu muutub funktsiooni foo lahendus canRejectOrReturn'i lahenduseks. Sellisel juhul koosneb kood vaid kahest read:

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

Kuid vaata, mis juhtub, kui kasutada koos await'i ja return'i:

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

Ülaltoodud koodis lõpetab foo edukalt nii täiusliku numbri kui ka kinni püütud veaga. Siin ei ole ebaõnnestumisi. Kuid foo lõpetab canRejectOrReturn'i, mitte undefined'iga. Veendugem selles, eemaldades rea return await canRejectOrReturn():

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

Levinud vead ja takistused

Mõnel juhul võib Async/Await'i kasutamine viia vigadeni.

Unustatud await

See juhtub piisavalt tihti — enne lubadust unustatakse võtmesõna await:

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

Koodis, nagu näha, ei ole ei await'i ega return'i. Seetõttu lõpetab foo alati undefined'iga ilma 1-sekundilise viivituseta. Kuid lubadus täidetakse. Kui aga see toob kaasa vea või reject'i, siis sellisel juhul kutsutakse esile UnhandledPromiseRejectionWarning.

Async-funktsioonid callback'ides

Async-funktsioone kasutatakse üsna sageli .map või .filter callbäck'ideks. Näiteks võib tuua funktsiooni fetchPublicReposCount(username), mis naaseb avatud GitHubi reposiitide arvu. Oletame, et meil on kolm kasutajat, kelle näitajad on vajalikud. Siin on kood selle ülesande jaoks:

const url = 'https://api.github.com/users';
 
// Kasutame funktsiooni, et hankida repo arvu
const fetchPublicReposCount = async (username) => {
  const response = await fetch(`${url}/${username}`);
  const json = await response.json();
  return json['public_repos'];
}

Me vajame kontosid ArfatSalman, octocat, norvig. Sellisel juhul teeme järgmist:

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

Oluline on märkida Await'i kasutamist .map tagasi kutsumises. Siin on counts – massiiv lubadustest, ning .map on anonüümne tagasiside iga nimega kasutaja kohta.

Liigne järgneva await'i kasutamine

Võtame näiteks järgmise koodi:

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 salvestatakse muutuja count'i rea repo arvu, seejärel lisatakse arv massiivi counts. Probleem seisneb selles, et kuni esimese kasutaja andmed serverist ei tule, on kõik järgnevad kasutajad ooteolukorras. Seega töödeldakse korraga vaid ühte kasutajat.

Kui ühe kasutaja töötlemiseks kulub umbes 300 ms, siis kõigi kasutajate puhul on see juba sekund, kulutatud aeg sõltub lineaarselt kasutajate arvust. Kuid kuna repode arv ei sõltu üksteisest, saab protsesse paralleelselt käivitada. Selleks on vajalik .map ja Promise.all kasutamine:

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 massiivi lubadusi, tagastades lubaduse. Viimane lõpeb pärast kõigi massiivis olevate lubaduste täitmist või esimese tagasilükkamise korral. Võib juhtuda, et need kõik ei käivitunud korraga — samaaegse käivituse tagamiseks võib kasutada p-map.

Kokkuvõte

Async-funktsioonid muutuvad arenduses üha olulisemaks. Ja et kasutada async-funktsioone tõhusalt, on mõistlik kasutada Async Iterators. JavaScripti arendaja peab sellest hästi aru saama.

Skillbox soovitab:

Allikas: habr.com

Osta usaldusväärne veebihosting DDoS kaitsega, VPS VDS serverid 🔥 Osta usaldusväärne veebihosting DDoS kaitsega, VPS VDS serverid | ProHoster