Авторът на статията разглежда примери за Async/Await в JavaScript. По принцип, Async/Await е удобен начин за писане на асинхронен код. Преди появата на тази възможност, подобен код се пишеше с използване на колбек функции и промиси. Авторът на оригиналната статия разкрива предимствата на Async/Await, разглеждайки различни примери.
Напомняме: за всички читатели на "Хабра" — отстъпка от 10 000 рубли при записване на всякакъв курс на 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 бяха добавени промисите. Те позволяват замяна на дълбоката вложеност на колбековете с думата .then.
Положителната страна на промисите е, че с тях кодът става значително по-четим, като се чете отгоре надолу, а не отляво надясно. Все пак, промисите също имат своите проблеми:
- Нужно е да се добавят много .then.
- Вместо try/catch се използва .catch за обработка на всички грешки.
- Работата с няколко промиса в един цикъл не винаги е удобна, в някои случаи те усложняват кода.
Ето задача, която ще покаже значението на последната точка.
Да предположим, че има цикъл for, който извежда последователност от числа от 0 до 10 със случайни интервали (0–n секунди). Използвайки обещания, трябва да променим този цикъл така, че числата да се показват в последователност от 0 до 10. Така че, ако изходът на нулата отнема 6 секунди, а единицата — 2 секунди, първо трябва да бъде изведена нулата, а след това да започне отброяването на единицата.
И разбира се, за решаване на тази задача не използваме Async/Await или .sort. Пример за решение — в края.
Async функции
Добавянето на async функции в ES2017 (ES8) опрости работата с обещанията. Забелязвам, че async функциите работят "над" обещанията. Тези функции не представляват качествено нови концепции. Async функциите са замислени като алтернатива на кода, който използва обещания.
Async/Await позволява организирането на работата с асинхронен код в синхронен стил.
Така че, познаването на обещанията улеснява разбирането на принципите на Async/Await.
Синтаксис
В обичайната ситуация той се състои от две ключови думи: async и await. Първата дума превръща функцията в асинхронна. В такива функции е разрешено използването на 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');
}
}NB! Трябва да помните, че конструкторите на класове и гетъри/сетъри не могат да бъдат асинхронни.
Семантика и правила на изпълнение
Async функциите, по принцип, са подобни на стандартните JS функции, но имат и изключения.
Така, async функциите винаги връщат обещания:
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.resolve ще се върне Promise.reject, съдържащ грешката.
Async функциите винаги дават промис на изхода, независимо от това какво се връща.
Асинхронните функции престават при всяко await.
Await оказва влияние на изразите. Ако изразът е промис, 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 = await Promise.resolve(9);.
- След използването на Await, изпълнението на функцията спира, докато а не получи стойността си (в текущия случай 9).
- delayAndGetRandom(1000) спира изпълнението на fn функцията, докато тя не приключи (след 1 секунда). Това всъщност е спиране на fn функцията за 1 секунда.
- delayAndGetRandom(1000) чрез resolve връща случаен валор, който след това се присвоява на променливата b.
- А случаят с променливата с е аналогичен на случая с променливата а. След това всичко спира за секунда, но сега 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 функции.
async function printNumbersUsingAsync() {
for (let i = 0; i < 10; i++) {
await wait(i, Math.random() * 1000);
console.log(i);
}
}Обработка на грешки
Необработените грешки се обвиват в rejected промис. Въпреки това, в async функциите може да се използва конструкцията try/catch, за да се извърши синхронна обработка на грешките.
async function canRejectOrReturn() {
// изчакайте една секунда
await new Promise(res => setTimeout(res, 1000));
// Отхвърлете с ~50% вероятност
if (Math.random() > 0.5) {
throw new Error('Съжалявам, числото е твърде голямо.')
}
return 'перфектно число';
}canRejectOrReturn() е асинхронна функция, която или успява (“перфектно число”), или завършва с грешка (“Съжалявам, числото е твърде голямо”).
async function foo() {
try {
await canRejectOrReturn();
} catch (e) {
return 'грешка уловена';
}
}Тъй като в примера по-горе се очаква изпълнението на canRejectOrReturn, самото неуспешно завършване ще доведе до изпълнение на блока catch. В резултат функцията foo ще завърши или с undefined (когато в блока try нищо не се връща), или с грешка уловена. В крайна сметка, тази функция няма да има неуспешно завършване, тъй като try/catch ще се погрижи за обработката на самата функция foo.
Ето още един пример:
async function foo() {
try {
return canRejectOrReturn();
} catch (e) {
return 'грешка уловена';
}
}Струва си да се обърне внимание на това, че в примера от foo се връща canRejectOrReturn. Foo в този случай завършва или с перфектно число, или с грешка Error (“Съжалявам, числото е твърде голямо”). Блокът catch никога няма да бъде изпълняван.
Проблемът е в това, че foo връща промис, предаден от canRejectOrReturn. Следователно решението на функцията foo става решението на canRejectOrReturn. В този случай кодът ще се състои само от две реда:
try {
const promise = canRejectOrReturn();
return promise;
}А ето какво ще се случи, ако използвате заедно await и return:
async function foo() {
try {
return await canRejectOrReturn();
} catch (e) {
return 'грешка уловена';
}
}В кода по-горе foo успешно ще завърши както с перфектно число, така и с грешка уловена. Тук откази няма да има. Но foo ще завърши с canRejectOrReturn, а не с undefined. Нека се уверим в това, като премахнем реда return await canRejectOrReturn():
try {
const value = await canRejectOrReturn();
return value;
}
// …Чести и подводни камъни
В някои случаи използването на Async/Await може да доведе до грешки.
Забравен await
Такова се случва достатъчно често — преди промис се забравя ключовата дума await:
async function foo() {
try {
canRejectOrReturn();
} catch (e) {
return 'caught';
}
}В кода, както е видно, няма нито await, нито return. Следователно foo винаги завършва с undefined без забавяне от 1 секунда. Но промисът ще се изпълнява. Ако той върне грешка или бъде отхвърлен, в този случай ще се генерира UnhandledPromiseRejectionWarning.
Async функции в обратни извиквания
Async функциите доста често се използват в .map или .filter като колбек функции. Като пример може да се приведе функцията fetchPublicReposCount(username), която връща броят на отворените репозитории в 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 е анонимен обратен извикване за всеки указан потребител.
Прекомерно последователно използване на await
Като пример вземаме такъв код:
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:
асинхронна функция fetchAllCounts(users) {
const promises = users.map(async username => {
const count = await fetchPublicReposCount(username);
return count;
});
return Promise.all(promises);
}Promise.all получава масив от промиси и връща промис. Последният завършва след приключване на всички промиси в масива или при първи reject. Може да се случи всичките да не стартират едновременно — за да се осигури едновременно стартиране, може да се използва p-map.
Заключение
Асинхронните функции стават все по-важни за разработката. А за адаптивно използване на асинхронни функции е добре да се възползвате от . JavaScript разработчикът трябва да има добро разбиране за това.
Skillbox препоръчва:
- Практически курс .
- Онлайн курс .
- Двугодишен практически курс .
Източник: habr.com
