Apskatīsim JavaScript/Async/Await, izmantojot piemērus

Raksta autors aplÅ«ko JavaScript/Async/Await piemērus. Kopumā Async/Await ir ērts veids, kā rakstÄ«t asinhrono kodu. Pirms Ŕīs funkcijas parādÄ«Å”anās Ŕāds kods tika rakstÄ«ts, izmantojot atzvanus un solÄ«jumus. Sākotnējā raksta autors, analizējot dažādus piemērus, atklāj Async/Await priekÅ”rocÄ«bas.

Atgādinām: visiem "Habr" lasītājiem - atlaide 10 000 rubļu, reģistrējoties jebkurā Skillbox kursā, izmantojot "Habr" reklāmas kodu.

Skillbox iesaka: IzglītojoŔs tieŔsaistes kurss "Java izstrādātājs".

Atzvans

AtzvanÄ«Å”ana ir funkcija, kuras zvans tiek aizkavēts uz nenoteiktu laiku. IepriekÅ” atzvanÄ«Å”ana tika izmantota tajos koda apgabalos, kur rezultātu nevarēja iegÅ«t uzreiz.

Å eit ir piemērs faila asinhronai lasÄ«Å”anai pakalpojumā Node.js:

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

Problēmas rodas, ja vienlaikus ir jāveic vairākas asinhronas darbÄ«bas. Iedomāsimies Ŕādu scenāriju: tiek veikts pieprasÄ«jums Arfat lietotāju datu bāzei, jāizlasa tās profils_img_url lauks un jālejupielādē attēls no servera someserver.com.
Pēc lejupielādes mēs pārvērÅ”am attēlu citā formātā, piemēram, no PNG uz JPEG. Ja konvertÄ“Å”ana bija veiksmÄ«ga, uz lietotāja e-pastu tiek nosÅ«tÄ«ta vēstule. Tālāk informācija par notikumu tiek ievadÄ«ta transformations.log failā, norādot datumu.

Ir vērts pievērst uzmanÄ«bu atzvanÄ«Å”anas pārklāŔanās un lielajam }) skaitam koda pēdējā daļā. To sauc par AtzvanÄ«Å”anas elli vai NolemtÄ«bas piramÄ«du.

Šīs metodes trūkumi ir acīmredzami:

  • Å o kodu ir grÅ«ti nolasÄ«t.
  • Ir arÄ« grÅ«ti rÄ«koties ar kļūdām, kas bieži noved pie sliktas koda kvalitātes.

Lai atrisinātu Å”o problēmu, JavaScript tika pievienoti solÄ«jumi. Tie ļauj aizstāt atzvanÄ«Å”anas dziļo ligzdoÅ”anu ar vārdu .then.

SolÄ«jumu pozitÄ«vais aspekts ir tas, ka tie padara kodu daudz labāk lasāmu, no augÅ”as uz leju, nevis no kreisās uz labo pusi. Tomēr solÄ«jumiem ir arÄ« savas problēmas:

  • Jums jāpievieno daudz .tad.
  • Try/catch vietā, lai apstrādātu visas kļūdas, tiek izmantots .catch.
  • Darbs ar vairākiem solÄ«jumiem vienā cilpā ne vienmēr ir ērti; dažos gadÄ«jumos tie sarežģī kodu.

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

Pieņemsim, ka mums ir for cilpa, kas izdrukā skaitļu virkni no 0 lÄ«dz 10 ar nejauÅ”iem intervāliem (0ā€“n sekundes). Izmantojot solÄ«jumus, Ŕī cilpa ir jāmaina, lai skaitļi tiktu drukāti secÄ«gi no 0 lÄ«dz 10. Tātad, ja nulles izdrukāŔanai nepiecieÅ”amas 6 sekundes un vieninieka izdrukāŔanai nepiecieÅ”amas 2 sekundes, vispirms jādrukā nulle un pēc tam sāksies atpakaļskaitÄ«Å”ana drukāŔanai.

Un, protams, mēs neizmantojam Async/Await vai .sort, lai atrisinātu Å”o problēmu. Risinājuma piemērs ir beigās.

Asinhronās funkcijas

Asinhrono funkciju pievienoÅ”ana ES2017 (ES8) vienkārÅ”oja uzdevumu strādāt ar solÄ«jumiem. Es atzÄ«mēju, ka asinhronās funkcijas darbojas ā€œpapildusā€ solÄ«jumiem. Å Ä«s funkcijas neatspoguļo kvalitatÄ«vi atŔķirÄ«gus jēdzienus. Asinhronās funkcijas ir paredzētas kā alternatÄ«va kodam, kas izmanto solÄ«jumus.

Async/Await ļauj organizēt darbu ar asinhrono kodu sinhronā stilā.

Tādējādi, zinot solījumus, ir vieglāk saprast Async/Await principus.

sintakse

Parasti tas sastāv no diviem atslēgvārdiem: async un await. Pirmais vārds pārvērÅ” funkciju par asinhronu. Šādas funkcijas ļauj izmantot gaidÄ«Å”anu. Jebkurā citā gadÄ«jumā Ŕīs funkcijas izmantoÅ”ana radÄ«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ācijas paŔā sākumā un bultiņas funkcijas gadÄ«jumā 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! Ir vērts atcerēties, ka klases konstruktori un ieguvēji/iestatītāji nevar būt asinhroni.

Semantika un izpildes noteikumi

Asinhronās funkcijas būtībā ir līdzīgas standarta JS funkcijām, taču ir izņēmumi.

Tādējādi asinhronās funkcijas vienmēr atgriež solījumus:

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

Konkrēti, fn atgriež virkni hello. Tā kā Ŕī ir asinhrona funkcija, virknes vērtÄ«ba tiek iesaiņota solÄ«jumā, izmantojot konstruktoru.

Šeit ir alternatīvs dizains bez async:

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

Å ajā gadÄ«jumā solÄ«jums tiek atgriezts ā€œmanuāliā€. Asinhronā funkcija vienmēr ir ietÄ«ta ar jaunu solÄ«jumu.

Ja atgrieÅ”anas vērtÄ«ba ir primitÄ«va, asinhronā funkcija atgriež vērtÄ«bu, iesaiņojot to solÄ«jumā. Ja atgriežamā vērtÄ«ba ir solÄ«juma objekts, tā izŔķirtspēja tiek atgriezta jaunā solÄ«jumā.

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

Bet kas notiek, ja asinhronajā funkcijā ir kļūda?

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

Ja tas netiek apstrādāts, foo() atgriezīs solījumu ar noraidījumu. Šādā situācijā Promise.resolve vietā tiks atgriezts Promise.reject, kurā ir kļūda.

Asinhronās funkcijas vienmēr izvada solījumu neatkarīgi no tā, kas tiek atgriezts.

Asinhronās funkcijas tiek apturētas katrā gaidÄ«Å”anas reizē.

GaidÄ«Å”ana ietekmē izteiksmes. Tātad, ja izteiksme ir solÄ«jums, asinhronā funkcija tiek apturēta, lÄ«dz tiek izpildÄ«ts solÄ«jums. Ja izteiksme nav solÄ«jums, tā tiek pārveidota par solÄ«jumu, izmantojot Promise.resolve, un pēc tam 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);

Un Ŕeit ir aprakstīts, kā darbojas fn funkcija.

  • Pēc tā izsaukÅ”anas pirmā rinda tiek pārveidota no const a = await 9; in const a = gaidiet Promise.resolve(9);.
  • Pēc Await izmantoÅ”anas funkcijas izpilde tiek apturēta, lÄ«dz a iegÅ«st savu vērtÄ«bu (paÅ”reizējā situācijā tā ir 9).
  • delayAndGetRandom(1000) aptur fn funkcijas izpildi, lÄ«dz tā tiek pabeigta pati (pēc 1 sekundes). Tas efektÄ«vi aptur fn funkciju uz 1 sekundi.
  • delayAndGetRandom(1000), izmantojot risinājumu, atgriež nejauÅ”u vērtÄ«bu, kas pēc tam tiek pieŔķirta mainÄ«gajam b.
  • GadÄ«jums ar mainÄ«go c ir lÄ«dzÄ«gs gadÄ«jumam ar mainÄ«go a. Pēc tam viss uz sekundi apstājas, bet tagad delayAndGetRandom(1000) neko neatgriež, jo tas nav nepiecieÅ”ams.
  • Rezultātā vērtÄ«bas tiek aprēķinātas, izmantojot formulu a + b * c. Rezultāts tiek iesaiņots solÄ«jumā, izmantojot Promise.resolve, un tiek atgriezts ar funkciju.

Šīs pauzes var atgādināt ES6 ģeneratorus, taču tajā ir kaut kas jūsu iemesli.

Problēmas risināŔana

Nu, tagad apskatÄ«sim iepriekÅ” minētās problēmas risinājumu.

Funkcija finishMyTask izmanto Await, lai gaidÄ«tu tādu darbÄ«bu rezultātus kā queryDatabase, sendEmail, logTaskInFile un citas. Ja salÄ«dzināsiet Å”o risinājumu ar to, kurā tika izmantoti solÄ«jumi, lÄ«dzÄ«bas kļūs acÄ«mredzamas. Tomēr Async/Await versija ievērojami vienkārÅ”o visas sintaktiskās sarežģītÄ«bas. Å ajā gadÄ«jumā nav daudz atzvanÄ«Å”anas gadÄ«jumu un ķēžu, piemēram, .then/.catch.

Šeit ir risinājums ar skaitļu izvadi, ir divas iespējas.

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

Un Ŕeit ir risinājums, izmantojot asinhronās funkcijas.

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

Kļūda apstrādē

Neapstrādātas kļūdas ir iesaiņotas noraidītā solījumā. Tomēr asinhronās funkcijas var izmantot try/catch, lai sinhroni apstrādātu kļūdas.

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() ir asinhrona funkcija, kas ir veiksmÄ«ga (ā€œideāls skaitlisā€) vai neizdodas ar kļūdu (ā€œAtvainojiet, skaitlis ir pārāk lielsā€).

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

Tā kā iepriekÅ” minētajā piemērā ir paredzēts izpildÄ«t canRejectOrReturn, tā paÅ”a kļūme izraisÄ«s uztverÅ”anas bloka izpildi. Rezultātā funkcija foo beigsies ar nedefinētu (kad nekas netiek atgriezts mēģinājuma blokā), vai arÄ« ar kļūdu. Rezultātā Ŕī funkcija neizdosies, jo try/catch apstrādās paÅ”u funkciju foo.

Šeit ir vēl viens piemērs:

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

Ir vērts pievērst uzmanÄ«bu faktam, ka piemērā canRejectOrReturn tiek atgriezts no foo. Foo Å”ajā gadÄ«jumā vai nu beidzas ar perfektu skaitli, vai atgriež kļūdu (ā€œAtvainojiet, skaitlis ir pārāk lielsā€). NoÄ·erÅ”anas bloks nekad netiks izpildÄ«ts.

Problēma ir tāda, ka foo atgriež solījumu, kas tika dots no canRejectOrReturn. Tātad risinājums foo kļūst par risinājumu canRejectOrReturn. Šajā gadījumā kods sastāvēs tikai no divām rindām:

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

Lūk, kas notiek, ja izmantojat gaidīŔanas un atgrieŔanās vienumus:

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

IepriekÅ” minētajā kodā foo veiksmÄ«gi izies gan ar perfektu numuru, gan pieÄ·erot kļūdu. Å eit nebÅ«s nekādu atteikumu. Bet foo atgriezÄ«sies ar canRejectOrReturn, nevis ar undefined. Pārliecināsimies par to, noņemot rindu return await canRejectOrReturn():

try {
    const value = await canRejectOrReturn();
    return value;
}
// ā€¦

Izplatītas kļūdas un kļūmes

Dažos gadījumos, izmantojot Async/Await, var rasties kļūdas.

Aizmirsts gaida

Tas notiek diezgan bieži - gaidÄ«Å”anas atslēgvārds tiek aizmirsts pirms solÄ«juma:

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

Kā redzat, kodā nav gaidÄ«Å”anas vai atgrieÅ”anas. Tāpēc foo vienmēr iziet ar undefined bez 1 sekundes aizkaves. Bet solÄ«jums tiks izpildÄ«ts. Ja tas rada kļūdu vai noraidÄ«Å”anu, tiks izsaukts UnhandledPromiseRejectionWarning.

Asinhronās funkcijas atzvanījumos

Asinhronās funkcijas diezgan bieži tiek izmantotas .map vai .filter kā atzvanÄ«Å”anas iespējas. Piemērs ir funkcija fetchPublicReposCount(lietotājvārds), kas atgriež GitHub atvērto repozitoriju skaitu. Pieņemsim, ka ir trÄ«s lietotāji, kuru rādÄ«tāji mums ir nepiecieÅ”ami. Å eit ir Ŕī uzdevuma kods:

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

Mums vajag ArfatSalman, octocat, norvig kontus. Šajā gadījumā mēs rīkojamies:

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

Ir vērts pievērst uzmanÄ«bu Await .map atzvanÄ«Å”anai. Å eit skaits ir solÄ«jumu klāsts, un .map ir anonÄ«ma atzvanÄ«Å”ana katram norādÄ«tajam lietotājam.

Pārāk konsekventa gaidīŔanas izmantoŔana

Ņemsim Å”o kodu kā piemēru:

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 repo numurs tiek ievietots skaitÄ«Å”anas mainÄ«gajā, pēc tam Å”is skaitlis tiek pievienots skaitÄ«Å”anas masÄ«vam. Problēma ar kodu ir tāda, ka lÄ«dz brÄ«dim, kad no servera pienāk pirmā lietotāja dati, visi nākamie lietotāji bÅ«s gaidÄ«Å”anas režīmā. Tādējādi vienlaikus tiek apstrādāts tikai viens lietotājs.

Ja, piemēram, viena lietotāja apstrāde aizņem apmēram 300 ms, tad visiem lietotājiem tā ir jau sekunde, patērētais laiks lineāri ir atkarÄ«gs no lietotāju skaita. Bet, tā kā repo skaita iegÅ«Å”ana nav viena no otras atkarÄ«ga, procesus var paralēli. Tam nepiecieÅ”ams strādāt ar .map un 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 kā ievadi saņem daudzus solÄ«jumus un atgriež solÄ«jumu. Pēdējais tiek izpildÄ«ts pēc tam, kad visi masÄ«vā solÄ«jumi ir izpildÄ«ti vai pēc pirmā noraidÄ«Å”anas. Var gadÄ«ties, ka tie visi nestartējas vienlaicÄ«gi ā€“ lai nodroÅ”inātu vienlaicÄ«gu startu, var izmantot p-map.

Secinājums

Asinhronās funkcijas kļūst arvien svarīgākas attīstībai. Lai adaptīvi izmantotu asinhronās funkcijas, jums vajadzētu izmantot Asinhronie iteratori. JavaScript izstrādātājam tas ir labi jāpārzina.

Skillbox iesaka:

Avots: www.habr.com

Pievieno komentāru