Ајде да погледнеме во Async/Await во JavaScript користејќи примери

Авторот на статијата испитува примери на Async/Await во JavaScript. Генерално, Async/Await е пригоден начин за пишување асинхрон код. Пред да се појави оваа функција, таков код беше напишан со помош на повратни повици и ветувања. Авторот на оригиналната статија ги открива предностите на Async/Await преку анализа на различни примери.

Потсетуваме: за сите читатели на „Хабр“ - попуст од 10 рубли при запишување на кој било курс Skillbox користејќи го промотивниот код „Хабр“.

Skillbox препорачува: Едукативен онлајн курс „Јава програмер“.

Повратен повик

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

Еве пример за асинхроно читање датотека во Node.js:

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

Проблемите се јавуваат кога треба да извршите неколку асинхрони операции одеднаш. Ајде да го замислиме ова сценарио: е поднесено барање до базата на податоци за корисници на Arfat, треба да го прочитате неговото поле profile_img_url и да преземете слика од серверот someserver.com.
По преземањето, ја претвораме сликата во друг формат, на пример од PNG во JPEG. Доколку конверзијата е успешна, се испраќа писмо до е-поштата на корисникот. Следно, информациите за настанот се внесуваат во датотеката transformations.log, означувајќи го датумот.

Вреди да се обрне внимание на преклопувањето на повратните повици и големиот број }) во последниот дел од кодот. Тоа се нарекува Callback Hell или Pyramid of Doom.

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

  • Овој код е тешко да се прочита.
  • Исто така е тешко да се справите со грешките, што често доведува до слаб квалитет на кодот.

За да се реши овој проблем, беа додадени ветувања на JavaScript. Тие ви дозволуваат да го замените длабокото вгнездување на повратни повици со зборот .тогаш.

Позитивниот аспект на ветувањата е што тие го прават кодот многу подобро читлив, од горе до долу, наместо од лево кон десно. Сепак, ветувањата имаат и свои проблеми:

  • Треба да додадете многу .потоа.
  • Наместо try/catch, .catch се користи за справување со сите грешки.
  • Работата со повеќе ветувања во една јамка не е секогаш погодна; во некои случаи, тие го комплицираат кодот.

Еве еден проблем што ќе го покаже значењето на последната точка.

Да претпоставиме дека имаме за јамка која печати низа од броеви од 0 до 10 во случајни интервали (0–n секунди). Користејќи ветувања, треба да ја промените оваа јамка така што броевите се печатат во низа од 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)
}
 

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

Забелешка! Вреди да се запамети дека конструкторите на класите и добивачите/поставувачите не можат да бидат асинхрони.

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

Асинхронизираните функции се во основа слични на стандардните 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

Во овој случај, ветувањето се враќа „рачно“. Асинхроната функција е секогаш завиткана во ново ветување.

Ако повратната вредност е примитивна, функцијата async ја враќа вредноста така што ќе ја завитка во ветување. Ако повратната вредност е предмет на ветување, нејзината резолуција се враќа во ново ветување.

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.reject што содржи грешка ќе се врати наместо Promise.resolve.

Async функциите секогаш даваат ветување, без оглед на тоа што се враќа.

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

Чекај влијае на изразите. Значи, ако изразот е ветување, функцијата async се суспендира додека не се исполни ветувањето. Ако изразот не е ветување, тој се претвора во ветување преку 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; во const a = чекаат Promise.resolve(9);.
  • По користењето на Await, извршувањето на функцијата се суспендира додека a не ја добие својата вредност (во сегашната ситуација е 9).
  • delayAndGetRandom(1000) го паузира извршувањето на функцијата fn додека таа не се комплетира (по 1 секунда). Ова ефикасно ја запира функцијата fn за 1 секунда.
  • delayAndGetRandom(1000) via solution враќа случајна вредност, која потоа се доделува на променливата b.
  • Па, случајот со променливата c е сличен на случајот со променливата a. После тоа, сè застанува за секунда, но сега delayAndGetRandom(1000) не враќа ништо бидејќи не е потребно.
  • Како резултат на тоа, вредностите се пресметуваат со формулата a + b * c. Резултатот е завиткан во ветување користејќи Promise.resolve и се враќа од функцијата.

Овие паузи можеби потсетуваат на генератори во ES6, но има нешто во тоа вашите причини.

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

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

Функцијата finishMyTask користи Await за да ги чека резултатите од операциите како што се queryDatabase, sendEmail, logTaskInFile и други. Ако го споредите ова решение со она каде што се користеа ветувања, сличностите ќе станат очигледни. Сепак, верзијата 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);
  }
}

Грешка при обработката

Нерешените грешки се завиткани во отфрлено ветување. Сепак, асинхронизираните функции може да користат обиди/фаќање за синхроно справување со грешките.

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, неговиот сопствен неуспех ќе резултира со извршување на блокот за фаќање. Како резултат на тоа, функцијата foo ќе заврши или со недефинирано (кога ништо не се враќа во блокот обид) или со фатена грешка. Како резултат на тоа, оваа функција нема да пропадне бидејќи обидот/фаќањето ќе се справи со самата функција 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 ќе се врати со canRejectOrReturn, а не со недефинирано. Ајде да се увериме во ова со отстранување на линијата за враќање на чекање canRejectOrReturn():

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

Вообичаени грешки и стапици

Во некои случаи, користењето Async/Await може да доведе до грешки.

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

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

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

Како што можете да видите, во кодот нема чекање или враќање. Затоа foo секогаш излегува со недефинирано без 1 секунда доцнење. Но, ветувањето ќе се исполни. Ако исфрли грешка или отфрлање, тогаш ќе се повика UnhandledPromiseRejectionWarning.

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

Асинхронизираните функции доста често се користат во .map или .filter како повратни повици. Пример е функцијата fetchPublicReposCount(корисничко име), која го враќа бројот на отворени складишта на GitHub. Да речеме дека има тројца корисници чии метрики ни се потребни. Еве го кодот за оваа задача:

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

Ни требаат сметки ArfatSalman, octocat, norvig. Во овој случај правиме:

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

Вреди да се обрне внимание на Await во .map повратен повик. Тука counts е низа од ветувања, а .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 ms за обработка на еден корисник, тогаш за сите корисници тоа е веќе втора; времето поминато линеарно зависи од бројот на корисници. Но, бидејќи добивањето на бројот на репо не зависи еден од друг, процесите може да се паралелизираат. Ова бара работа со .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-map.

Заклучок

Асинхронизираните функции стануваат сè поважни за развојот. Па, за адаптивна употреба на асинхрони функции, треба да користите Асинхронизирани итератори. Развивачот на JavaScript треба да биде добро упатен во ова.

Skillbox препорачува:

Извор: www.habr.com

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