Келгиле, мисалдарды колдонуу менен JavaScript'те Async/Awaitти карап көрөлү

Макаланын автору JavaScriptдеги Async/Await мисалдарын карап чыгат. Жалпысынан алганда, Async/Await асинхрондук кодду жазуунун ыңгайлуу жолу. Бул функция пайда болгонго чейин, мындай код кайра чалуулар жана убадалар аркылуу жазылган. Түпнуска макаланын автору ар кандай мисалдарды талдоо менен Async/Awaitтин артыкчылыктарын ачып берет.

Биз эсиңизге салабыз: "Хабрдын" бардык окурмандары үчүн - "Habr" промо-кодун колдонуу менен каалаган Skillbox курсуна катталганда 10 000 рубль арзандатуу.

Skillbox сунуштайт: Онлайн билим берүү курсу "Java иштеп чыгуучу".

чалуу

Кайра чалуу - бул чалуу белгисиз мөөнөткө кечигүү функциясы. Мурда кайра чалуулар коддун натыйжасын дароо алуу мүмкүн болбогон аймактарда колдонулган.

Бул жерде Node.js файлын асинхрондуу окуунун мисалы:

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

Бир эле учурда бир нече асинхрондук операцияларды аткаруу керек болгондо көйгөйлөр пайда болот. Бул сценарийди элестетип көрөлү: Арфат колдонуучу базасына суроо-талап жасалат, анын profile_img_url талаасын окуп, someserver.com серверинен сүрөттү жүктөп алышыңыз керек.
Жүктөп алгандан кийин биз сүрөттү башка форматка, мисалы PNG форматынан JPEG форматына которабыз. Эгерде конверсия ийгиликтүү болсо, кат колдонуучунун электрондук почтасына жөнөтүлөт. Андан кийин, окуя жөнүндө маалымат transformations.log файлына датасын көрсөтүү менен киргизилет.

Коддун акыркы бөлүгүндө кайра чалуулардын кайталанышына жана көп сандагы }) көңүл буруу зарыл. Бул кайра чалуулар тозогу же кыямат пирамидасы деп аталат.

Бул ыкманын кемчиликтери көрүнүп турат:

  • Бул кодду окуу кыйын.
  • Ошондой эле каталарды чечүү кыйын, бул көбүнчө коддун сапатынын начардыгына алып келет.

Бул көйгөйдү чечүү үчүн JavaScript'ке убадалар кошулду. Алар кайра чалуулардын терең уясын .then деген сөз менен алмаштырууга мүмкүндүк берет.

Убадалардын позитивдүү жагы, алар кодду солдон оңго эмес, өйдөдөн ылдый карай бир топ жакшыраак окулат. Бирок, убадалардын да өз көйгөйлөрү бар:

  • Сиз көп .андан кийин кошуу керек.
  • try/catch ордуна, .catch бардык каталарды иштетүү үчүн колдонулат.
  • Бир цикл ичинде бир нече убадалар менен иштөө кээ бир учурларда дайыма эле ыңгайлуу боло бербейт, алар кодду татаалдаштырат.

Бул жерде акыркы пункттун маанисин көрсөтө турган маселе.

Бизде 0дөн 10го чейинки сандар ырааттуулугун туш келди интервалдарда (0–n секунда) басып чыгарган for цикли бар дейли. Убадаларды колдонуу менен, бул циклди сандар 0дөн 10го чейин ырааттуу басып чыгаруу үчүн өзгөртүү керек. Демек, нөлдү басып чыгаруу үчүн 6 секунд, бирди басып чыгаруу үчүн 2 секунд талап кылынса, анда нөл алгач, анан басылышы керек. басып чыгаруу үчүн артка санак башталат.

Жана, албетте, биз бул маселени чечүү үчүн Async/Await же .sort колдонбойбуз. Мисал чечим аягында.

Асинхрондуу функциялар

ES2017де (ES8) асинхрондук функцияларды кошуу убадалар менен иштөө милдетин жөнөкөйлөттү. Мен асинхрондук функциялар убадалардын "үстүндө" иштей тургандыгын белгилеймин. Бул функциялар сапаттык жактан ар башка түшүнүктөрдү билдирбейт. Асинхрондук функциялар убадаларды колдонгон кодго альтернатива катары арналган.

Async/Await синхрондуу стилде асинхрондук код менен ишти уюштурууга мүмкүндүк берет.

Ошентип, убадаларды билүү Async/Await принциптерин түшүнүүнү жеңилдетет.

синтаксиси

Адатта ал эки ачкыч сөздөн турат: синхрондоштуруу жана күтүү. Биринчи сөз функцияны асинхрондукка айлантат. Мындай функциялар күтүүнү колдонууга мүмкүндүк берет. Башка учурда, бул функцияны колдонуу катаны жаратат.

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

Асинхрондук функцияны жарыялоонун эң башында, ал эми жебе функциясы болсо, “=” белгиси менен кашаанын арасына киргизилет.

Бул функциялар объектке методдор катары жайгаштырылышы мүмкүн же класс декларациясында колдонулушу мүмкүн.

// 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! Класстын конструкторлору жана алуучулар/жөндөөчүлөр асинхрондуу боло албастыгын эстен чыгарбоо керек.

Семантика жана аткаруу эрежелери

Асинхрондук функциялар негизинен стандарттуу JS функцияларына окшош, бирок өзгөчөлүктөр бар.

Ошентип, асинхрондуу функциялар ар дайым убадаларды кайтарат:

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

Тактап айтканда, fn hello сабын кайтарат. Ооба, бул асинхрондук функция болгондуктан, саптын мааниси конструктордун жардамы менен убадага оролот.

Бул жерде Async жок альтернативдүү дизайн:

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

Бул учурда, убада "кол менен" кайтарылат. Асинхрондук функция ар дайым жаңы убадага оролгон.

Кайтаруучу маани примитив болсо, асинхрондук функция аны убадага ороп, маанини кайтарат. Эгерде кайтаруу мааниси убада объекти болсо, анын резолюциясы жаңы убадага кайтарылат.

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

Бирок асинхрондук функциянын ичинде ката болсо эмне болот?

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

Эгерде ал иштетилбесе, foo() убаданы четке кагуу менен кайтарат. Бул жагдайда, Promise.resolve ордуна ката камтылган Promise.reject кайтарылып берилет.

Асинхрондук функциялар кайтарылган нерсеге карабастан, ар дайым убаданы чыгарат.

Асинхрондук функциялар ар бир күтүүдө тыным алат.

Күтүү туюнтмаларга таасир этет. Демек, сөз айкашы убада болсо, асинхрондук функция убада аткарылмайынча токтотулат. Эгерде сөз айкашы убада эмес болсо, анда ал Promise.resolve аркылуу убадага айландырылат жана андан соң аягына чыгарылат.

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

Жана бул жерде fn функциясынын кантип иштээри сүрөттөлөт.

  • Аны чакыргандан кийин, биринчи сап const a = await 9дан которулат; in const a = Wait Promise.resolve(9);.
  • Await колдонгондон кийин, функциянын аткарылышы a өз маанисин алганга чейин токтотулат (учурдагы кырдаалда 9).
  • delayAndGetRandom(1000) fn функциясынын аткарылышын ал өзү аяктаганга чейин (1 секунддан кийин) токтотот. Бул fn функциясын 1 секундга натыйжалуу токтотот.
  • delayAndGetRandom(1000) чечүү аркылуу кокустук маанини кайтарат, ал андан кийин b өзгөрмөсүнө дайындалат.
  • Ооба, c өзгөрмөлүү окуя a өзгөрмөлүү окуяга окшош. Андан кийин баары бир секундага токтойт, бирок азыр delayAndGetRandom(1000) эч нерсе кайтарбайт, анткени талап кылынбайт.
  • Натыйжада, баалуулуктар a + b * c формуласы менен эсептелет. Натыйжа Promise.resolve аркылуу убадага оролуп, функция тарабынан кайтарылат.

Бул тыныгуулар ES6дагы генераторлорду эске салышы мүмкүн, бирок ага бир нерсе бар сиздин себептериңиз.

Проблеманы чечүү

Эми жогоруда айтылган маселенин чечилишин карап көрөлү.

finishMyTask функциясы queryDatabase, sendEmail, logTaskInFile жана башкалар сыяктуу операциялардын натыйжаларын күтүү үчүн Await функциясын колдонот. Эгерде сиз бул чечимди убадалар колдонулган чечим менен салыштырсаңыз, окшоштуктар айкын болот. Бирок, Async/Await версиясы бардык синтаксистик татаалдыктарды абдан жөнөкөйлөтөт. Бул учурда, .then/.catch сыяктуу кайра чалуулардын жана чынжырлардын көп саны жок.

Бул жерде сандарды чыгаруу менен чечим, эки вариант бар.

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

Жана бул жерде асинхрондук функцияларды колдонгон чечим.

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

Ката иштетүү

Иштетилбеген каталар четке кагылган убадага оролгон. Бирок, асинхрондук функциялар каталарды синхрондуу иштетүү үчүн try/catch колдоно алат.

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() – бул асинхрондук функция, ал ийгиликтүү (“мыкты сан”) же ката менен иштебей калат (“Кечиресиз, сан өтө чоң”).

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

Жогорудагы мисал canRejectOrReturn аткарышын күткөндүктөн, анын өз катасы catch блогунун аткарылышына алып келет. Натыйжада, foo функциясы же аныкталбаган (try блогунда эч нерсе кайтарылбаганда) же ката табылган менен аяктайт. Натыйжада, бул функция иштебей калат, анткени try/catch foo функциясын өзү башкарат.

Бул жерде дагы бир мисал:

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

Мисалда canRejectOrReturn fooдан кайтарылганына көңүл буруш керек. Бул учурда Foo же идеалдуу сан менен аяктайт же Ката кайтарат (“Кечиресиз, сан өтө чоң”). Кармоо блогу эч качан аткарылбайт.

Маселе fooнун canRejectOrReturn берген убадасын кайтарып алуусунда. Ошентип, foo чечими canRejectOrReturn үчүн чечим болуп калат. Бул учурда, код эки гана саптан турат:

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

Эгерде сиз күтүү жана кайтуу функциясын чогуу колдонсоңуз, эмне болот:

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

Жогорудагы коддо, foo кемчиликсиз сан жана ката менен ийгиликтүү чыгат. Бул жерде эч кандай баш тартуу болбойт. Бирок foo undefined менен эмес, canRejectOrReturn менен кайтып келет. Келгиле, кайра күтүү canRejectOrReturn() сабын алып салуу менен муну текшерели:

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

Жалпы каталар жана тузактар

Кээ бир учурларда, Async/Await колдонуу каталарга алып келиши мүмкүн.

Күтүү унутта калды

Бул көп учурда болот - await ачкыч сөз убададан мурун унутулуп калат:

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

Көрүнүп тургандай, коддо күтүү же кайтаруу жок. Ошондуктан foo ар дайым 1 секунд кечиктирбестен аныкталбаган менен чыгат. Бирок убада аткарылат. Эгер ал ката же четке кагылса, UnhandledPromiseRejectionWarning деп аталат.

Кайра чалуулардагы асинхрондуу функциялар

Асинхрондук функциялар көбүнчө .map же .filterде кайра чалуу катары колдонулат. Мисал GitHubдагы ачык репозиторийлердин санын кайтарган fetchPublicReposCount(username) функциясы. Бизге метрикалары керек болгон үч колдонуучу бар дейли. Бул тапшырма үчүн код:

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

АрфатСалман, октокат, норвиг аккаунттары керек. Бул учурда биз жасайбыз:

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

.map кайра чалуудагы Await дегенге көңүл буруш керек. Бул жерде эсептер - бул убадалардын жыйындысы, ал эми .map - ар бир көрсөтүлгөн колдонуучу үчүн анонимдүү кайра чалуу.

күтүүнү ашыкча ырааттуу колдонуу

Мисал катары бул кодду алалы:

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

Бул жерде репо номери count өзгөрмөсүнө жайгаштырылат, андан кийин бул сан counts массивине кошулат. Коддогу көйгөй биринчи колдонуучунун маалыматтары серверден келгенге чейин, бардык кийинки колдонуучулар күтүү режиминде болот. Ошентип, бир эле учурда бир гана колдонуучу иштетилет.

Эгер, мисалы, бир колдонуучуну иштетүү үчүн 300 мс керек болсо, анда бардык колдонуучулар үчүн бул убакыт сызыктуу түрдө колдонуучулардын санына жараша болот; Бирок репо санын алуу бири-биринен көз каранды болбогондуктан, процесстерди параллелдештирүүгө болот. Бул .map жана 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 киргизүү катары көптөгөн убадаларды алат жана убаданы кайтарат. Акыркысы, массивдеги бардык убадалар аткарылгандан кийин же биринчи четке кагылганда аткарылат. Алардын баары бир убакта башталбай калышы мүмкүн - бир убакта баштоону камсыз кылуу үчүн, сиз p-картаны колдонсоңуз болот.

жыйынтыктоо

Асинхрондук функциялар өнүгүү үчүн барган сайын маанилүү болуп баратат. Ооба, асинхрондук функцияларды адаптациялоо үчүн колдонууга арзырлык Асинхрондуу Итераторлор. JavaScript иштеп чыгуучусу муну жакшы билиши керек.

Skillbox сунуштайт:

Source: www.habr.com

Комментарий кошуу