Ајде да погледнеме во Async/Await во JavaScript користејќи примери
Авторот на статијата испитува примери на Async/Await во JavaScript. Генерално, Async/Await е пригоден начин за пишување асинхрон код. Пред да се појави оваа функција, таков код беше напишан со помош на повратни повици и ветувања. Авторот на оригиналната статија ги открива предностите на Async/Await преку анализа на различни примери.
Потсетуваме:за сите читатели на „Хабр“ - попуст од 10 рубли при запишување на кој било курс Skillbox користејќи го промотивниот код „Хабр“.
Повратен повик е функција чиј повик се одложува на неодредено време. Претходно, повратните повици се користеа во оние области на кодот каде што резултатот не можеше да се добие веднаш.
Еве пример за асинхроно читање датотека во Node.js:
Проблемите се јавуваат кога треба да извршите неколку асинхрони операции одеднаш. Ајде да го замислиме ова сценарио: е поднесено барање до базата на податоци за корисници на 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() е асинхрона функција која или успева („совршен број“) или не успева со грешка („Извини, бројката е преголема“).
Бидејќи примерот погоре очекува да се изврши canRejectOrReturn, неговиот сопствен неуспех ќе резултира со извршување на блокот за фаќање. Како резултат на тоа, функцијата foo ќе заврши или со недефинирано (кога ништо не се враќа во блокот обид) или со фатена грешка. Како резултат на тоа, оваа функција нема да пропадне бидејќи обидот/фаќањето ќе се справи со самата функција foo.
Вреди да се обрне внимание на фактот дека во примерот, canRejectOrReturn се враќа од foo. Foo во овој случај или завршува со совршен број или враќа Грешка („Извинете, бројката е преголема“). Блокот за фаќање никогаш нема да се изврши.
Проблемот е што foo го враќа ветувањето дадено од canRejectOrReturn. Така, решението за foo станува решение за canRejectOrReturn. Во овој случај, кодот ќе се состои од само две линии:
Во горната шифра, foo ќе излезе успешно и со совршен број и со фатена грешка. Овде нема да има одбивања. Но, foo ќе се врати со canRejectOrReturn, а не со недефинирано. Ајде да се увериме во ова со отстранување на линијата за враќање на чекање canRejectOrReturn():
Како што можете да видите, во кодот нема чекање или враќање. Затоа foo секогаш излегува со недефинирано без 1 секунда доцнење. Но, ветувањето ќе се исполни. Ако исфрли грешка или отфрлање, тогаш ќе се повика UnhandledPromiseRejectionWarning.
Асинхронизирани функции во повратни повици
Асинхронизираните функции доста често се користат во .map или .filter како повратни повици. Пример е функцијата fetchPublicReposCount(корисничко име), која го враќа бројот на отворени складишта на GitHub. Да речеме дека има тројца корисници чии метрики ни се потребни. Еве го кодот за оваа задача:
Вреди да се обрне внимание на 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:
Promise.all добива низа ветувања како влез и враќа ветување. Последново, откако ќе се завршат сите ветувања во низата или при првото одбивање, е завршено. Може да се случи сите да не започнат истовремено - за да обезбедите истовремен почеток, можете да користите p-map.
Заклучок
Асинхронизираните функции стануваат сè поважни за развојот. Па, за адаптивна употреба на асинхрони функции, треба да користите Асинхронизирани итератори. Развивачот на JavaScript треба да биде добро упатен во ова.