Le të shohim Async/Await në JavaScript duke përdorur shembuj

Autori i artikullit shqyrton shembuj të Async/Await në JavaScript. Në përgjithësi, Async/Await është një mënyrë e përshtatshme për të shkruar kodin asinkron. Para se të shfaqej kjo veçori, një kod i tillë ishte shkruar duke përdorur kthime thirrjesh dhe premtime. Autori i artikullit origjinal zbulon avantazhet e Async/Await duke analizuar shembuj të ndryshëm.

Kujtojmë: për të gjithë lexuesit e "Habr" - një zbritje prej 10 rubla kur regjistroheni në çdo kurs Skillbox duke përdorur kodin promovues "Habr".

Skillbox rekomandon: Kurs edukativ online "Zhvilluesi Java".

Callback

Kthimi i thirrjes është një funksion, thirrja e të cilit vonohet për një kohë të pacaktuar. Më parë, kthimet e thirrjeve përdoreshin në ato zona të kodit ku rezultati nuk mund të merrej menjëherë.

Këtu është një shembull i leximit asinkron të një skedari në Node.js:

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

Problemet lindin kur duhet të kryeni disa operacione asinkrone në të njëjtën kohë. Le të imagjinojmë këtë skenar: një kërkesë i bëhet bazës së të dhënave të përdoruesve Arfat, duhet të lexoni fushën e saj profile_img_url dhe të shkarkoni një imazh nga serveri someserver.com.
Pas shkarkimit, ne e konvertojmë imazhin në një format tjetër, për shembull nga PNG në JPEG. Nëse konvertimi ishte i suksesshëm, një letër dërgohet në emailin e përdoruesit. Më pas, informacioni për ngjarjen futet në skedarin transformations.log, duke treguar datën.

Vlen t'i kushtohet vëmendje mbivendosjes së thirrjeve dhe numrit të madh të }) në pjesën e fundit të kodit. Quhet Callback Hell ose Piramida e Kijametit.

Disavantazhet e kësaj metode janë të dukshme:

  • Ky kod është i vështirë për t'u lexuar.
  • Është gjithashtu e vështirë të trajtohen gabimet, gjë që shpesh çon në cilësi të dobët të kodit.

Për të zgjidhur këtë problem, premtimet u shtuan në JavaScript. Ato ju lejojnë të zëvendësoni folenë e thellë të thirrjeve me fjalën .pastaj.

Aspekti pozitiv i premtimeve është se ato e bëjnë kodin shumë më mirë të lexueshëm, nga lart poshtë dhe jo nga e majta në të djathtë. Megjithatë, edhe premtimet kanë problemet e tyre:

  • Duhet të shtoni shumë .pastaj.
  • Në vend të try/catch, .catch përdoret për të trajtuar të gjitha gabimet.
  • Puna me premtime të shumta brenda një cikli nuk është gjithmonë e përshtatshme; në disa raste, ato e ndërlikojnë kodin.

Këtu është një problem që do të tregojë kuptimin e pikës së fundit.

Supozoni se kemi një cikli for që printon një sekuencë numrash nga 0 në 10 në intervale të rastësishme (0–n sekonda). Duke përdorur premtimet, ju duhet ta ndryshoni këtë lak në mënyrë që numrat të shtypen në sekuencë nga 0 në 10. Pra, nëse duhen 6 sekonda për të printuar një zero dhe 2 sekonda për të printuar një, fillimisht duhet të printohet zeroja dhe më pas do të fillojë numërimi mbrapsht për printimin e atij.

Dhe sigurisht, ne nuk përdorim Async/Await ose .sort për të zgjidhur këtë problem. Një zgjidhje shembull është në fund.

Funksionet asinkronike

Shtimi i funksioneve asinkronike në ES2017 (ES8) thjeshtoi detyrën e punës me premtimet. Unë vërej se funksionet asinkronike funksionojnë "mbi" premtimet. Këto funksione nuk përfaqësojnë koncepte cilësisht të ndryshme. Funksionet asinkronike synohen si një alternativë ndaj kodit që përdor premtime.

Async/Await bën të mundur organizimin e punës me kodin asinkron në një stil sinkron.

Kështu, njohja e premtimeve e bën më të lehtë kuptimin e parimeve të Async/Await.

sintaksë

Normalisht përbëhet nga dy fjalë kyçe: asinkronizuar dhe prit. Fjala e parë e kthen funksionin në asinkron. Funksione të tilla lejojnë përdorimin e pritjes. Në çdo rast tjetër, përdorimi i këtij funksioni do të gjenerojë një gabim.

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

Async futet në fillim të deklaratës së funksionit, dhe në rastin e funksionit me shigjetë, midis shenjës "=" dhe kllapave.

Këto funksione mund të vendosen në një objekt si metoda ose të përdoren në një deklaratë 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! Vlen të kujtohet se konstruktorët e klasave dhe marrës/vendosjet nuk mund të jenë asinkron.

Semantika dhe rregullat e ekzekutimit

Funksionet asinkronike janë në thelb të ngjashme me funksionet standarde JS, por ka përjashtime.

Kështu, funksionet asinkronike gjithmonë kthejnë premtime:

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

Në mënyrë të veçantë, fn kthen vargun hello. Epo, meqenëse ky është një funksion asinkron, vlera e vargut mbështillet në një premtim duke përdorur një konstruktor.

Këtu është një dizajn alternativ pa Async:

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

Në këtë rast, premtimi kthehet "me dorë". Një funksion asinkron është gjithmonë i mbështjellë me një premtim të ri.

Nëse vlera e kthimit është primitive, funksioni async e kthen vlerën duke e mbështjellë atë në një premtim. Nëse vlera e kthimit është një objekt premtimi, rezolucioni i tij kthehet në një premtim të ri.

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

Por çfarë ndodh nëse ka një gabim brenda një funksioni asinkron?

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

Nëse nuk përpunohet, foo() do të kthejë një premtim me refuzim. Në këtë situatë, Promise.reject që përmban një gabim do të kthehet në vend të Promise.resolve.

Funksionet Async japin gjithmonë një premtim, pavarësisht nga ajo që kthehet.

Funksionet asinkrone ndalojnë në çdo pritje.

Prisja ndikon në shprehjet. Pra, nëse shprehja është premtim, funksioni asinkronik pezullohet derisa premtimi të përmbushet. Nëse shprehja nuk është një premtim, ajo konvertohet në një premtim nëpërmjet Promise.resolve dhe më pas plotësohet.

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

Dhe këtu është një përshkrim se si funksionon funksioni fn.

  • Pas thirrjes së saj, rreshti i parë konvertohet nga const a = await 9; në const a = presim Premtimin.zgjidh (9);.
  • Pas përdorimit të Await, ekzekutimi i funksionit pezullohet derisa a të marrë vlerën e tij (në situatën aktuale është 9).
  • delayAndGetRandom(1000) ndalon ekzekutimin e funksionit fn derisa ai të përfundojë vetë (pas 1 sekonde). Kjo në mënyrë efektive ndalon funksionin fn për 1 sekondë.
  • delayAndGetRandom(1000) via zgjidhje kthen një vlerë të rastësishme, e cila më pas i caktohet ndryshores b.
  • Epo, rasti me ndryshoren c është i ngjashëm me rastin me ndryshoren a. Pas kësaj, gjithçka ndalon për një sekondë, por tani delayAndGetRandom(1000) nuk kthen asgjë sepse nuk kërkohet.
  • Si rezultat, vlerat llogariten duke përdorur formulën a + b * c. Rezultati mbështillet në një premtim duke përdorur Promise.resolve dhe kthehet nga funksioni.

Këto pauza mund të kujtojnë gjeneratorët në ES6, por ka diçka në të arsyet tuaja.

Zgjidhja e problemit

Epo, tani le të shohim zgjidhjen e problemit të përmendur më lart.

Funksioni finishMyTask përdor Await për të pritur rezultatet e operacioneve të tilla si queryDatabase, sendEmail, logTaskInFile dhe të tjera. Nëse e krahasoni këtë zgjidhje me atë ku janë përdorur premtimet, ngjashmëritë do të bëhen të dukshme. Sidoqoftë, versioni Async/Await thjeshton shumë të gjitha kompleksitetet sintaksore. Në këtë rast, nuk ka një numër të madh të thirrjeve dhe zinxhirëve si .then/.catch.

Këtu është një zgjidhje me daljen e numrave, ka dy opsione.

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

Dhe këtu është një zgjidhje duke përdorur funksionet asinkronike.

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

Gabim gjatë përpunimit

Gabimet e patrajtuara mbështillen me një premtim të refuzuar. Megjithatë, funksionet asinkronike mund të përdorin try/catch për të trajtuar gabimet në mënyrë sinkronike.

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() është një funksion asinkron që ose ka sukses (“numër i përsosur”) ose dështon me një gabim (“Na falni, numri shumë i madh”).

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

Meqenëse shembulli i mësipërm pret që canRejectOrReturn të ekzekutohet, dështimi i tij do të rezultojë në ekzekutimin e bllokut të kapjes. Si rezultat, funksioni foo do të përfundojë ose me të papërcaktuar (kur asgjë nuk kthehet në bllokun e provës) ose me një gabim të kapur. Si rezultat, ky funksion nuk do të dështojë sepse try/catch do të trajtojë vetë funksionin foo.

Ja një shembull tjetër:

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

Vlen t'i kushtohet vëmendje faktit se në shembull, canRejectOrReturn është kthyer nga foo. Foo në këtë rast ose përfundon me një numër të përsosur ose kthen një Gabim ("Na vjen keq, numri shumë i madh"). Blloku i kapjes nuk do të ekzekutohet kurrë.

Problemi është se foo kthen premtimin e dhënë nga canRejectOrReturn. Pra, zgjidhja për foo bëhet zgjidhja për mund të RejectOrReturn. Në këtë rast, kodi do të përbëhet nga vetëm dy rreshta:

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

Ja çfarë ndodh nëse përdorni pritjen dhe ktheheni së bashku:

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

Në kodin e mësipërm, foo do të dalë me sukses me një numër të përsosur dhe një gabim të kapur. Këtu nuk do të ketë refuzime. Por foo do të kthehet me canRejectOrReturn, jo me të papërcaktuara. Le të sigurohemi për këtë duke hequr vijën e pritjes së kthimit canRejectOrReturn():

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

Gabimet dhe kurthet e zakonshme

Në disa raste, përdorimi i Async/Prit mund të çojë në gabime.

Pritja e harruar

Kjo ndodh mjaft shpesh - fjala kyçe e pritjes harrohet para premtimit:

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

Siç mund ta shihni, nuk ka pritje ose kthim në kod. Prandaj foo del gjithmonë me të papërcaktuara pa një vonesë 1 sekondë. Por premtimi do të realizohet. Nëse paraqet një gabim ose refuzim, atëherë do të thirret UnhandledPromiseRejectionWarning.

Funksionet e asinkronizuara në kthimet e thirrjeve

Funksionet asinkronike përdoren mjaft shpesh në .map ose .filter si kthime të thirrjeve. Një shembull është funksioni fetchPublicReposCount(emri i përdoruesit), i cili kthen numrin e depove të hapura në GitHub. Le të themi se ka tre përdorues, metrikat e të cilëve na duhen. Këtu është kodi për këtë detyrë:

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

Ne kemi nevojë për llogari ArfatSalman, octocat, norvig. Në këtë rast bëjmë:

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

Vlen t'i kushtohet vëmendje "Prit" në kthimin e thirrjes .map. Këtu numëron një sërë premtimesh dhe .map është një kthim anonim për çdo përdorues të specifikuar.

Përdorimi tepër i qëndrueshëm i prit

Le të marrim këtë kod si shembull:

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

Këtu numri i repos vendoset në variablin count, më pas ky numër i shtohet grupit të numërimit. Problemi me kodin është se derisa të dhënat e përdoruesit të parë të mbërrijnë nga serveri, të gjithë përdoruesit e mëpasshëm do të jenë në modalitetin e gatishmërisë. Kështu, vetëm një përdorues përpunohet në të njëjtën kohë.

Nëse, për shembull, duhen rreth 300 ms për të përpunuar një përdorues, atëherë për të gjithë përdoruesit është tashmë një i dytë; koha e shpenzuar varet në mënyrë lineare nga numri i përdoruesve. Por meqenëse marrja e numrit të repove nuk varet nga njëra-tjetra, proceset mund të paralelizohen. Kjo kërkon të punosh me .map dhe 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 merr një sërë premtimesh si hyrje dhe kthen një premtim. Kjo e fundit plotësohet pasi të gjitha premtimet në grup janë përfunduar ose në refuzimin e parë. Mund të ndodhë që të gjithë të mos fillojnë në të njëjtën kohë - për të siguruar fillimin e njëkohshëm, mund të përdorni p-map.

Përfundim

Funksionet asinkronike po bëhen gjithnjë e më të rëndësishme për zhvillim. Epo, për përdorim adaptiv të funksioneve asinkronike ia vlen të përdoret Përsëritësit e asinkronizuar. Një zhvillues JavaScript duhet të jetë i përgatitur mirë në këtë.

Skillbox rekomandon:

Burimi: www.habr.com

Shto një koment