Хајде да погледамо Асинц/Аваит у ЈаваСцрипт-у користећи примере

Аутор чланка испитује примере Асинц/Аваит у ЈаваСцрипт-у. Све у свему, Асинц/Аваит је згодан начин за писање асинхроног кода. Пре него што се ова функција појавила, такав код је написан коришћењем повратних позива и обећања. Аутор оригиналног чланка открива предности Асинц/Аваит анализом различитих примера.

Подсећамо: за све читаоце „Хабра“ – попуст од 10 рубаља при упису на било који курс Скиллбок користећи промотивни код „Хабр“.

Скиллбок препоручује: Образовни онлајн курс "Јава програмер".

Повратни позив

Повратни позив је функција чији се позив одлаже на неодређено време. Раније су повратни позиви коришћени у оним областима кода где се резултат није могао добити одмах.

Ево примера асинхроног читања датотеке у Ноде.јс:

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

Проблеми настају када је потребно да извршите неколико асинхроних операција одједном. Замислимо овај сценарио: захтев се шаље бази података корисника Арфата, потребно је да прочитате њено поље профиле_имг_урл и преузмете слику са сервера сомесервер.цом.
Након преузимања, конвертујемо слику у други формат, на пример из ПНГ у ЈПЕГ. Ако је конверзија била успешна, на е-маил корисника се шаље писмо. Затим се информације о догађају уносе у датотеку трансформатионс.лог, наводећи датум.

Вреди обратити пажњу на преклапање повратних позива и велики број }) у завршном делу кода. Зове се пакао повратног позива или пирамида пропасти.

Недостаци ове методе су очигледни:

  • Овај код је тежак за читање.
  • Такође је тешко руковати грешкама, што често доводи до лошег квалитета кода.

Да би се решио овај проблем, ЈаваСцрипту су додана обећања. Они вам омогућавају да замените дубоко угнежђење повратних позива речју .тхен.

Позитиван аспект обећања је да чине код много боље читљивијим, одозго према доле, а не с лева на десно. Међутим, обећања имају и своје проблеме:

  • Морате додати много .онда.
  • Уместо три/цатцх, .цатцх се користи за обраду свих грешака.
  • Рад са више обећања унутар једне петље није увек згодан, у неким случајевима они компликују код.

Ево проблема који ће показати значење последње тачке.

Претпоставимо да имамо фор петљу која штампа низ бројева од 0 до 10 у насумичним интервалима (0–н секунди). Користећи обећања, треба да промените ову петљу тако да се бројеви штампају у низу од 0 до 10. Дакле, ако је за штампање нуле потребно 6 секунди и за штампање јединице 2 секунде, прво треба да се одштампа нула, а затим почиње одбројавање за штампање.

И наравно, не користимо Асинц/Аваит или .сорт да решимо овај проблем. Пример решења је на крају.

Асинхронизоване функције

Додавање асинхронизованих функција у ЕС2017 (ЕС8) поједноставило је задатак рада са обећањима. Примећујем да асинхронизоване функције раде „на врху“ обећања. Ове функције не представљају квалитативно различите концепте. Асинхронизоване функције су замишљене као алтернатива коду који користи обећања.

Асинц/Аваит омогућава организовање рада са асинхроним кодом у синхроном стилу.

Дакле, познавање обећања олакшава разумевање принципа Асинц/Аваит.

синтакса

Обично се састоји од две кључне речи: асинц и аваит. Прва реч претвара функцију у асинхрону. Такве функције омогућавају употребу чекања. У сваком другом случају, коришћење ове функције ће генерисати грешку.

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

НБ! Вреди запамтити да конструктори класа и геттери/сеттери не могу бити асинхрони.

Семантика и правила извршења

Асинхронизоване функције су у основи сличне стандардним ЈС функцијама, али постоје изузеци.

Дакле, асинхронизоване функције увек враћају обећања:

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

Конкретно, фн враћа стринг хелло. Па, пошто је ово асинхрона функција, вредност стринга је умотана у обећање помоћу конструктора.

Ево алтернативног дизајна без Асинц-а:

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

Ако се не обради, фоо() ће вратити обећање са одбијањем. У овој ситуацији, Промисе.рејецт који садржи грешку биће враћен уместо Промисе.ресолве.

Асинхронизоване функције увек дају обећање, без обзира на то шта се враћа.

Асинхроне функције паузирају на сваком чекању.

Чекање утиче на изразе. Дакле, ако је израз обећање, асинц функција се суспендује док се обећање не испуни. Ако израз није обећање, конвертује се у обећање преко Промисе.ресолве и затим завршава.

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

А ево и описа како функционише функција фн.

  • Након што га позовете, први ред се конвертује из цонст а = аваит 9; ин цонст а = аваит Промисе.ресолве(9);.
  • Након употребе Аваит, извршење функције се суспендује док а не добије своју вредност (у тренутној ситуацији је 9).
  • делаиАндГетРандом(1000) паузира извршавање функције фн док се не заврши (после 1 секунде). Ово ефективно зауставља функцију фн на 1 секунду.
  • делаиАндГетРандом(1000) преко ресолве враћа насумичне вредности, која се затим додељује променљивој б.
  • Па, случај са променљивом ц је сличан случају са променљивом а. Након тога, све се зауставља на секунду, али сада делаиАндГетРандом(1000) не враћа ништа јер није потребно.
  • Као резултат, вредности се израчунавају помоћу формуле а + б * ц. Резултат је умотан у обећање користећи Промисе.ресолве и враћа га функција.

Ове паузе можда подсећају на генераторе у ЕС6, али има нешто у томе своје разлоге.

Решавање проблема

Па, хајде сада да погледамо решење за горе поменути проблем.

Функција финисхМиТаск користи Аваит да сачека резултате операција као што су куериДатабасе, сендЕмаил, логТаскИнФиле и друге. Ако упоредите ово решење са оним где су коришћена обећања, сличности ће постати очигледне. Међутим, верзија Асинц/Аваит у великој мери поједностављује све синтаксичке сложености. У овом случају, нема великог броја повратних позива и ланаца као што је .тхен/.цатцх.

Ево решења са излазом бројева, постоје две опције.

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

Грешка у обради

Необрађене грешке су умотане у одбијено обећање. Међутим, асинхроне функције могу да користе три/цатцх за синхроно руковање грешкама.

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

цанРејецтОрРетурн() је асинхрона функција која или успе („савршен број“) или не успе са грешком („Извини, број је превелик“).

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

Пошто горњи пример очекује да се цанРејецтОрРетурн изврши, његов сопствени неуспех ће довести до извршења блока цатцх. Као резултат, функција фоо ће се завршити или са недефинисаном (када ништа није враћено у блоку три) или са ухваћеном грешком. Као резултат, ова функција неће успети јер ће три/цатцх сама руковати функцијом фоо.

Ево још једног примера:

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

Вреди обратити пажњу на чињеницу да се у примеру цанРејецтОрРетурн враћа из фоо. Фоо се у овом случају или завршава савршеним бројем или враћа грешку („Извините, број је превелик“). Блок цатцх никада неће бити извршен.

Проблем је у томе што фоо враћа обећање које је предао цанРејецтОрРетурн. Дакле, решење за фоо постаје решење за цанРејецтОрРетурн. У овом случају, код ће се састојати од само два реда:

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

Ево шта се дешава ако користите чекање и повратак заједно:

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

У коду изнад, фоо ће успешно изаћи и са савршеним бројем и са ухваћеном грешком. Овде неће бити одбијања. Али фоо ће се вратити са цанРејецтОрРетурн, а не са недефинисаним. Хајде да се уверимо у ово тако што ћемо уклонити линију ретурн аваит цанРејецтОрРетурн():

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

Уобичајене грешке и замке

У неким случајевима, коришћење Асинц/Аваит може довести до грешака.

Заборављено чекање

Ово се дешава прилично често - кључна реч аваит се заборавља пре обећања:

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

Као што видите, у коду нема чекања или враћања. Због тога фоо увек излази са недефинисаним без кашњења од 1 секунде. Али обећање ће бити испуњено. Ако произведе грешку или одбијање, биће позван УнхандледПромисеРејецтионВарнинг.

Асинхронизоване функције у повратним позивима

Асинц функције се често користе у .мап или .филтер као повратни позиви. Пример је функција фетцхПублицРепосЦоунт(усернаме), која враћа број отворених спремишта на ГитХуб-у. Рецимо да постоје три корисника чије су нам метрике потребне. Ево кода за овај задатак:

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

Вреди обратити пажњу на Аваит у повратном позиву .мап. Овде цоунтс представља низ обећања, а .мап је анонимни повратни позив за сваког одређеног корисника.

Претерано доследна употреба чекања

Узмимо овај код као пример:

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

Овде се репо број ставља у променљиву цоунт, а затим се овај број додаје низу цоунтс. Проблем са кодом је у томе што ће сви наредни корисници бити у стању приправности док подаци првог корисника не стигну са сервера. Дакле, истовремено се обрађује само један корисник.

Ако је, на пример, потребно око 300 мс за обраду једног корисника, онда је то за све кориснике већ секунда утрошено време линеарно зависи од броја корисника. Али пошто добијање броја репо не зависи једно од другог, процеси се могу паралелизирати. Ово захтева рад са .мап и Промисе.алл:

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

Промисе.алл прима низ обећања као улаз и враћа обећање. Ово последње, након што су сва обећања у низу завршена или при првом одбијању, је завршена. Може се десити да сви не почну истовремено - да бисте обезбедили истовремени почетак, можете користити п-мапу.

Закључак

Асинхронизоване функције постају све важније за развој. Па, за адаптивно коришћење асинхронизованих функција вреди га користити Асинц Итераторс. ЈаваСцрипт програмер би требао бити добро упућен у ово.

Скиллбок препоручује:

Извор: ввв.хабр.цом

Додај коментар