Tingnan natin ang Async/Await sa JavaScript gamit ang mga halimbawa

Sinusuri ng may-akda ng artikulo ang mga halimbawa ng Async/Await sa JavaScript. Sa pangkalahatan, ang Async/Await ay isang maginhawang paraan upang magsulat ng asynchronous na code. Bago lumitaw ang feature na ito, isinulat ang naturang code gamit ang mga callback at pangako. Inihayag ng may-akda ng orihinal na artikulo ang mga pakinabang ng Async/Await sa pamamagitan ng pagsusuri sa iba't ibang halimbawa.

Pinapaalala namin sa iyo: para sa lahat ng mga mambabasa ng "Habr" - isang diskwento na 10 rubles kapag nag-enroll sa anumang kurso sa Skillbox gamit ang code na pang-promosyon ng "Habr".

Inirerekomenda ng Skillbox ang: Online na kursong pang-edukasyon "Java developer".

Callback

Ang callback ay isang function na ang tawag ay naantala nang walang katiyakan. Dati, ang mga callback ay ginamit sa mga lugar ng code kung saan hindi agad makukuha ang resulta.

Narito ang isang halimbawa ng asynchronous na pagbabasa ng isang file sa Node.js:

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

Lumilitaw ang mga problema kapag kailangan mong magsagawa ng ilang mga asynchronous na operasyon nang sabay-sabay. Isipin natin ang sitwasyong ito: ang isang kahilingan ay ginawa sa database ng gumagamit ng Arfat, kailangan mong basahin ang field na profile_img_url nito at mag-download ng isang imahe mula sa server ng someserver.com.
Pagkatapos mag-download, iko-convert namin ang larawan sa ibang format, halimbawa mula sa PNG patungo sa JPEG. Kung matagumpay ang conversion, magpapadala ng sulat sa email ng user. Susunod, ang impormasyon tungkol sa kaganapan ay ipinasok sa transformations.log file, na nagpapahiwatig ng petsa.

Ito ay nagkakahalaga ng pagbibigay pansin sa overlap ng mga callback at ang malaking bilang ng }) sa huling bahagi ng code. Ito ay tinatawag na Callback Hell o Pyramid of Doom.

Ang mga kawalan ng pamamaraang ito ay halata:

  • Mahirap basahin ang code na ito.
  • Mahirap ding pangasiwaan ang mga error, na kadalasang humahantong sa hindi magandang kalidad ng code.

Upang malutas ang problemang ito, idinagdag ang mga pangako sa JavaScript. Nagbibigay-daan sa iyo ang mga ito na palitan ang malalim na nesting ng mga callback ng salitang .then.

Ang positibong aspeto ng mga pangako ay ginagawa nilang mas nababasa ang code, mula sa itaas hanggang sa ibaba sa halip na mula kaliwa hanggang kanan. Gayunpaman, ang mga pangako ay mayroon ding mga problema:

  • Kailangan mong magdagdag ng maraming .pagkatapos.
  • Sa halip na try/catch, ginagamit ang .catch para pangasiwaan ang lahat ng error.
  • Ang pagtatrabaho sa maraming mga pangako sa loob ng isang loop ay hindi palaging maginhawa; sa ilang mga kaso, ginagawang kumplikado ang code.

Narito ang isang problema na magpapakita ng kahulugan ng huling punto.

Ipagpalagay na mayroon tayong for loop na nagpi-print ng pagkakasunod-sunod ng mga numero mula 0 hanggang 10 sa mga random na pagitan (0–n segundo). Gamit ang mga pangako, kailangan mong baguhin ang loop na ito upang ang mga numero ay naka-print sa pagkakasunud-sunod mula 0 hanggang 10. Kaya, kung aabutin ng 6 na segundo upang mag-print ng zero at 2 segundo upang mag-print ng isa, ang zero ay dapat na i-print muna, at pagkatapos magsisimula na ang countdown para sa pag-print ng isa.

At siyempre, hindi kami gumagamit ng Async/Await o .sort para lutasin ang problemang ito. Ang isang halimbawang solusyon ay nasa dulo.

Mga function ng Async

Ang pagdaragdag ng mga function ng async sa ES2017 (ES8) ay pinasimple ang gawain ng pagtatrabaho sa mga pangako. Napansin kong gumagana ang mga async function "sa itaas" ng mga pangako. Ang mga pag-andar na ito ay hindi kumakatawan sa magkakaibang mga konsepto. Ang mga function ng Async ay inilaan bilang isang alternatibo sa code na gumagamit ng mga pangako.

Ginagawang posible ng Async/Await na ayusin ang trabaho gamit ang asynchronous na code sa isang kasabay na istilo.

Kaya, ang pag-alam sa mga pangako ay nagpapadali sa pag-unawa sa mga prinsipyo ng Async/Await.

palaugnayan

Karaniwang binubuo ito ng dalawang keyword: async at naghihintay. Ang unang salita ay ginagawang asynchronous ang function. Ang ganitong mga pag-andar ay nagpapahintulot sa paggamit ng paghihintay. Sa anumang iba pang kaso, ang paggamit ng function na ito ay bubuo ng error.

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

Ang Async ay ipinapasok sa pinakadulo simula ng deklarasyon ng function, at sa kaso ng isang arrow function, sa pagitan ng "=" sign at ng mga panaklong.

Ang mga function na ito ay maaaring ilagay sa isang bagay bilang mga pamamaraan o ginagamit sa isang deklarasyon ng klase.

// 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! Ito ay nagkakahalaga ng pag-alala na ang mga konstruktor ng klase at getter/setters ay hindi maaaring maging asynchronous.

Semantika at mga panuntunan sa pagpapatupad

Ang mga function ng Async ay karaniwang katulad ng mga karaniwang function ng JS, ngunit may mga pagbubukod.

Kaya, ang mga function ng async ay palaging nagbabalik ng mga pangako:

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

Sa partikular, ibinabalik ng fn ang string na hello. Well, dahil ito ay isang asynchronous function, ang string value ay nakabalot sa isang pangako gamit ang isang constructor.

Narito ang isang alternatibong disenyo na walang Async:

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

Sa kasong ito, ang pangako ay ibinalik nang "manu-mano". Ang isang asynchronous na function ay palaging nakabalot sa isang bagong pangako.

Kung primitive ang return value, ibinabalik ng async function ang value sa pamamagitan ng pag-wrap nito sa isang pangako. Kung ang return value ay promise object, ang resolution nito ay ibabalik sa isang bagong promise.

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

Ngunit ano ang mangyayari kung mayroong isang error sa loob ng isang asynchronous na function?

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

Kung hindi ito naproseso, ang foo() ay magbabalik ng pangako na may pagtanggi. Sa sitwasyong ito, ibabalik ang Promise.reject na naglalaman ng error sa halip na Promise.resolve.

Ang mga function ng Async ay palaging naglalabas ng isang pangako, anuman ang ibinalik.

Ang mga asynchronous na function ay naka-pause sa bawat paghihintay.

Ang paghihintay ay nakakaapekto sa mga ekspresyon. Kaya, kung ang expression ay isang pangako, ang async function ay sinuspinde hanggang sa ang pangako ay natupad. Kung ang expression ay hindi isang pangako, ito ay iko-convert sa isang pangako sa pamamagitan ng Promise.resolve at pagkatapos ay makumpleto.

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

At narito ang isang paglalarawan kung paano gumagana ang fn function.

  • Pagkatapos tawagan ito, ang unang linya ay na-convert mula sa const a = naghihintay 9; sa const a = maghintay Promise.resolve(9);.
  • Pagkatapos gamitin ang Await, ang pagsasagawa ng function ay sinuspinde hanggang sa makuha ng a ang halaga nito (sa kasalukuyang sitwasyon ito ay 9).
  • Pino-pause ng delayAndGetRandom(1000) ang execution ng fn function hanggang sa makumpleto nito ang sarili nito (pagkatapos ng 1 segundo). Ito ay epektibong huminto sa fn function sa loob ng 1 segundo.
  • Ang delayAndGetRandom(1000) sa pamamagitan ng resolve ay nagbabalik ng random na halaga, na pagkatapos ay itinalaga sa variable b.
  • Well, ang kaso na may variable c ay katulad ng case na may variable a. Pagkatapos nito, huminto ang lahat ng isang segundo, ngunit ngayon ay wala nang ibinabalik ang delayAndGetRandom(1000) dahil hindi ito kinakailangan.
  • Bilang resulta, ang mga halaga ay kinakalkula gamit ang formula a + b * c. Ang resulta ay nakabalot sa isang pangako gamit ang Promise.resolve at ibinalik ng function.

Ang mga pag-pause na ito ay maaaring nakapagpapaalaala sa mga generator sa ES6, ngunit mayroong isang bagay dito iyong mga dahilan.

Paglutas ng problema

Well, ngayon tingnan natin ang solusyon sa problemang nabanggit sa itaas.

Ang finishMyTask function ay gumagamit ng Await para maghintay para sa mga resulta ng mga operasyon gaya ng queryDatabase, sendEmail, logTaskInFile, at iba pa. Kung ihahambing mo ang solusyon na ito sa isa kung saan ginamit ang mga pangako, magiging halata ang pagkakatulad. Gayunpaman, lubos na pinapasimple ng bersyon ng Async/Await ang lahat ng mga kumplikadong syntactic. Sa kasong ito, walang malaking bilang ng mga callback at chain tulad ng .then/.catch.

Narito ang isang solusyon sa output ng mga numero, mayroong dalawang mga pagpipilian.

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

At narito ang isang solusyon gamit ang mga function ng async.

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

Error sa pagproseso

Ang mga hindi nahawakang pagkakamali ay nababalot sa isang tinanggihang pangako. Gayunpaman, ang mga function ng async ay maaaring gumamit ng try/catch upang pangasiwaan ang mga error nang sabay-sabay.

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

Ang canRejectOrReturn() ay isang asynchronous na function na maaaring magtagumpay (β€œperpektong numero”) o nabigo nang may error (β€œPaumanhin, masyadong malaki ang numero”).

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

Dahil ang halimbawa sa itaas ay inaasahan na maipatupad ang canRejectOrReturn, ang sarili nitong kabiguan ay magreresulta sa pagpapatupad ng catch block. Bilang resulta, ang function foo ay magtatapos sa alinman sa hindi natukoy (kapag walang ibinalik sa try block) o may nahuli na error. Bilang resulta, hindi mabibigo ang function na ito dahil ang try/catch ang hahawak sa function foo mismo.

Narito ang isa pang halimbawa:

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

Ito ay nagkakahalaga ng pagbibigay pansin sa katotohanan na sa halimbawa, ang canRejectOrReturn ay ibinalik mula sa foo. Ang Foo sa kasong ito ay maaaring magwawakas sa isang perpektong numero o magbabalik ng Error ("Paumanhin, masyadong malaki ang numero"). Ang catch block ay hindi kailanman isasagawa.

Ang problema ay ibinabalik ni foo ang ipinasa na pangako mula sa canRejectOrReturn. Kaya ang solusyon sa foo ay nagiging solusyon sa canRejectOrReturn. Sa kasong ito, ang code ay bubuo lamang ng dalawang linya:

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

Narito ang mangyayari kung gagamit ka ng await at babalik nang magkasama:

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

Sa code sa itaas, matagumpay na lalabas ang foo na may parehong perpektong numero at isang error na nahuli. Walang mga pagtanggi dito. Ngunit babalik ang foo na may canRejectOrReturn, hindi na may undefined. Siguraduhin natin ito sa pamamagitan ng pag-alis sa linya ng pagbabalik na naghihintay sa canRejectOrReturn():

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

Mga karaniwang pagkakamali at pitfalls

Sa ilang mga kaso, ang paggamit ng Async/Await ay maaaring humantong sa mga error.

Nakalimutang naghihintay

Madalas itong nangyayari - ang naghihintay na keyword ay nakalimutan bago ang pangako:

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

Tulad ng nakikita mo, walang naghihintay o bumalik sa code. Samakatuwid ang foo ay laging lumalabas nang hindi natukoy nang walang 1 segundong pagkaantala. Ngunit ang pangako ay matutupad. Kung magtapon ito ng error o pagtanggi, tatawagin ang UnhandledPromiseRejectionWarning.

Mga Async Function sa Mga Callback

Ang mga function ng Async ay kadalasang ginagamit sa .map o .filter bilang mga callback. Ang isang halimbawa ay ang fetchPublicReposCount(username) function, na nagbabalik ng bilang ng mga bukas na repository sa GitHub. Sabihin nating mayroong tatlong user na ang mga sukatan ay kailangan natin. Narito ang code para sa gawaing ito:

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

Kailangan namin ng ArfatSalman, octocat, norvig account. Sa kasong ito ginagawa namin:

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

Ito ay nagkakahalaga ng pagbibigay pansin sa Maghintay sa .map callback. Narito ang bilang ng isang hanay ng mga pangako, at ang .map ay isang hindi kilalang callback para sa bawat tinukoy na user.

Masyadong pare-pareho ang paggamit ng paghihintay

Kunin natin ang code na ito bilang isang halimbawa:

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

Dito inilalagay ang numero ng repo sa variable ng bilang, pagkatapos ay idinagdag ang numerong ito sa array ng mga bilang. Ang problema sa code ay hanggang sa dumating ang data ng unang user mula sa server, ang lahat ng susunod na user ay nasa standby mode. Kaya, isang user lang ang pinoproseso sa isang pagkakataon.

Kung, halimbawa, aabutin ng humigit-kumulang 300 ms upang maproseso ang isang user, kung gayon para sa lahat ng mga user ay isa na itong segundo; ang oras na ginugugol nang linear ay depende sa bilang ng mga user. Ngunit dahil ang pagkuha ng bilang ng repo ay hindi nakasalalay sa isa't isa, ang mga proseso ay maaaring parallelized. Nangangailangan ito ng pagtatrabaho sa .map at Promise.all:

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

Ang Promise.all ay tumatanggap ng hanay ng mga pangako bilang input at nagbabalik ng pangako. Ang huli, pagkatapos makumpleto ang lahat ng mga pangako sa array o sa unang pagtanggi, ay nakumpleto. Maaaring mangyari na ang lahat ng mga ito ay hindi nagsisimula sa parehong oras - upang matiyak ang sabay-sabay na pagsisimula, maaari mong gamitin ang p-map.

Konklusyon

Ang mga function ng Async ay nagiging lalong mahalaga para sa pag-unlad. Well, para sa adaptive na paggamit ng async function, dapat mong gamitin Mga Async Iterator. Ang isang developer ng JavaScript ay dapat na bihasa dito.

Inirerekomenda ng Skillbox ang:

Pinagmulan: www.habr.com

Magdagdag ng komento