Autor artykułu omawia na przykładach Async/Await w JavaScript. Ogólnie rzecz biorąc, Async/Await to wygodny sposób pisania kodu asynchronicznego. Przed pojawieniem tej możliwości kod asynchroniczny pisano za pomocą callbacków i obietnic. Autor oryginalnego artykułu ujawnia zalety Async/Await, analizując różne przykłady.
Przypominamy: dla wszystkich czytelników „Habra” — zniżka 10 000 rubli przy zapisie na dowolny kurs Skillbox z kodem promocyjnym „Habra”.
Skillbox poleca: Edukacyjny kurs online .
Callback
Callback to funkcja, której wywołanie jest opóźnione na nieokreślony czas. Wcześniej callbacki wykorzystywano w tych częściach kodu, w których wynik nie mógł być uzyskany od razu.
Oto przykład asynchronicznego odczytu pliku w Node.js:
fs.readFile(__filename, 'utf-8', (err, data) => {
if (err) {
throw err;
}
console.log(data);
}); Problemy pojawiają się, gdy trzeba wykonać kilka asynchronicznych operacji jednocześnie. Wyobraźmy sobie taki scenariusz: wykonywane jest zapytanie do bazy danych użytkownika Arfat, trzeba odczytać jego pole profile_img_url i załadować obrazek z serwera someserver.com.
Po załadowaniu konwertujemy obrazek na inny format, na przykład z PNG na JPEG. Jeżeli konwersja zakończyła się powodzeniem, do użytkownika wysyłany jest e-mail. Następnie informacja o zdarzeniu jest zapisywana w pliku transformations.log z datą.
Warto zwrócić uwagę na zagnieżdżanie callbacków i dużą liczbę } w końcowej części kodu. To nazywa się Callback Hell lub Pyramid of Doom.
Wady tego sposobu są oczywiste:
- Ten kod jest trudny do przeczytania.
- Trudno również obsługiwać błędy, co często prowadzi do pogorszenia jakości kodu.
Aby rozwiązać ten problem, w JavaScript dodano obietnice. Pozwalają one zastąpić głębokie zagnieżdżenie callbacków słowem .then.
Pozytywną stroną obietnic jest to, że kod z nimi jest znacznie bardziej czytelny, przynajmniej od góry do dołu, a nie od lewej do prawej. Niemniej jednak obietnice również mają swoje problemy:
- Trzeba dodawać dużą liczbę .then.
- Zamiast try/catch używa się .catch do obsługi wszystkich błędów.
- Praca z wieloma obietnicami w ramach jednej pętli nie zawsze jest wygodna, w niektórych przypadkach komplikuje kod.
Oto zadanie, które pokaże znaczenie ostatniego punktu.
Załóżmy, że jest pętla for, która wyświetla sekwencję liczb od 0 do 10 z losowym interwałem (0–n sekund). Używając obietnic, musimy zmodyfikować tę pętlę, aby liczby były wyświetlane w kolejności od 0 do 10. Tak więc, jeśli wyświetlenie zera zajmuje 6 sekund, a jedynki 2 sekundy, najpierw powinno być wyświetlone zero, a potem zacznie się odliczanie wyświetlenia jedynki.
Oczywiście, w rozwiązaniu tego zadania nie używamy Async/Await ani .sort. Przykład rozwiązania znajduje się na końcu.
Funkcje async
Dodanie funkcji async w ES2017 (ES8) uprościło pracę z obietnicami. Zauważę, że funkcje async działają „nad” obietnicami. Te funkcje nie są jakościowo inną koncepcją. Funkcje async były zaprojektowane jako alternatywa dla kodu, który używa obietnic.
Async/Await umożliwia organizację pracy z asynchronicznym kodem w stylu synchronicznym.
Zatem znajomość obietnic ułatwia zrozumienie zasad Async/Await.
Składnia
W zwykłej sytuacji składa się z dwóch słów kluczowych: async i await. Pierwsze słowo przekształca funkcję w asynchroniczną. W takich funkcjach dozwolone jest użycie await. W przeciwnym razie użycie tej funkcji spowoduje błąd.
// With function declaration
async function myFn() {
// await ...
}
// With arrow function
const myFn = async () => {
// await ...
}
function myFn() {
// await fn(); (Syntax Error since no async)
}
Async jest umieszczane na początku deklaracji funkcji, a w przypadku użycia funkcji strzałkowej — między znakiem „=” a nawiasami.
Te funkcje można umieścić w obiekcie jako metody lub użyć w deklaracji klasy.
// 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! Należy pamiętać, że konstruktory klas oraz gettery/settery nie mogą być asynchroniczne.
Semantyka i zasady wykonywania
Funkcje async są zasadniczo podobne do standardowych funkcji JS, ale są też wyjątki.
Tak, funkcje async zawsze zwracają obietnice:
async function fn() {
return 'hello';
}
fn().then(console.log)
// helloW szczególności fn zwraca ciąg 'hello'. A ponieważ jest to funkcja asynchroniczna, wartość ciągu jest opakowywana w obietnicę za pomocą konstruktora.
Oto alternatywna konstrukcja bez Async:
function fn() {
return Promise.resolve('hello');
}
fn().then(console.log);
// helloW tym przypadku zwracanie obietnicy odbywa się „ręcznie”. Funkcja asynchroniczna zawsze jest opakowywana w nową obietnicę.
W przypadku, gdy wartość zwracana jest prymitywem, funkcja async zwraca tę wartość, owijając ją w promis. W przypadku, gdy wartość zwracana jest obiektem promis, jego rozwiązanie jest zwracane w nowym promis.
const p = Promise.resolve('hello')
p instanceof Promise;
// true
Promise.resolve(p) === p;
// true
Co się stanie, jeśli w wewnętrznej funkcji asynchronicznej wystąpi błąd?
async function foo() {
throw Error('bar');
}
foo().catch(console.log);Jeśli nie zostanie obsłużony, foo() zwróci promis z odrzuceniem. W tej sytuacji zamiast Promise.resolve zwróci się Promise.reject zawierający błąd.
Funkcje async zawsze zwracają promis, niezależnie od tego, co jest zwracane.
Funkcje asynchroniczne są wstrzymywane przy każdym await.
Await wpływa na wyrażenia. Jeśli wyrażenie jest promisem, funkcja async jest wstrzymywana do momentu wykonania promisa. W przypadku, gdy wyrażenie nie jest promisem, jest konwertowane na promis za pomocą Promise.resolve i następnie zwracane.
// 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);A oto opis, jak działa funkcja fn.
- Po jej wywołaniu pierwsza linia jest konwertowana z const a = await 9; na const a = await Promise.resolve(9);.
- Po użyciu Await wykonanie funkcji zostaje wstrzymane, dopóki a nie otrzyma swojej wartości (w tej sytuacji to 9).
- delayAndGetRandom(1000) wstrzymuje wykonanie funkcji fn, aż sama się zakończy (po 1 sekundzie). To faktycznie jest zatrzymaniem funkcji fn na 1 sekundę.
- delayAndGetRandom(1000) zwraca przez resolve losową wartość, która następnie jest przypisywana do zmiennej b.
- Z kolei przypadek zmiennej c jest analogiczny do przypadku zmiennej a. Po tym wszystkim wykonanie zatrzymuje się na sekundę, ale teraz delayAndGetRandom(1000) nic nie zwraca, ponieważ nie jest to potrzebne.
- W rezultacie wartości są obliczane według formuły a + b * c. Wynik jest owijany w promis przy pomocy Promise.resolve i zwracany przez funkcję.
Te pauzy mogą przypominać generatory w ES6, ale mają swoje .
Rozwiązując problem
A teraz przyjrzyjmy się rozwiązaniu problemu, który został wcześniej wymieniony.
W funkcji finishMyTask użyto Await do oczekiwania na wyniki takich operacji, jak queryDatabase, sendEmail, logTaskInFile i innych. Jeśli porównamy to rozwiązanie z tym, gdzie używano obietnic, podobieństwo będzie oczywiste. Niemniej jednak wersja z Async/Await znacznie upraszcza wszystkie złożoności składniowe. W tym przypadku nie ma dużej liczby callbacków ani łańcuchów takich jak .then/.catch.
Oto rozwiązanie do wyświetlania liczb, istnieją dwa warianty.
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);
});
});
};A oto rozwiązanie z użyciem funkcji async.
async function printNumbersUsingAsync() {
for (let i = 0; i < 10; i++) {
await wait(i, Math.random() * 1000);
console.log(i);
}
}Obsługa błędów
Nieobsłużone błędy są owijane w odrzuconą obietnicę. Niemniej jednak w funkcjach async można użyć konstrukcji try/catch, aby wykonać synchroniczną obsługę błędów.
async function canRejectOrReturn() {
// czekaj jedną sekundę
await new Promise(res => setTimeout(res, 1000));
// Odrzuć z prawdopodobieństwem ~50%
if (Math.random() > 0.5) {
throw new Error('Przykro mi, liczba zbyt duża.')
}
return 'idealna liczba';
}canRejectOrReturn() to funkcja asynchroniczna, która albo wykonuje się pomyślnie („idealna liczba”), albo kończy się niepowodzeniem z błędem („Przykro mi, liczba zbyt duża”).
async function foo() {
try {
await canRejectOrReturn();
} catch (e) {
return 'wycatchowany błąd';
}
}Ponieważ w powyższym przykładzie oczekiwane jest wykonanie canRejectOrReturn, własne niepowodzenie spowoduje uruchomienie bloku catch. W rezultacie funkcja foo zakończy się albo jako undefined (gdy w bloku try nic nie jest zwracane), albo jako wycatchowany błąd. W efekcie ta funkcja nie będzie miała niepowodzenia, ponieważ try/catch zajmie się przetwarzaniem samej funkcji foo.
Oto kolejny przykład:
async function foo() {
try {
return canRejectOrReturn();
} catch (e) {
return 'wycatchowany błąd';
}
}Należy zauważyć, że w przykładzie z foo zwraca się canRejectOrReturn. Foo w tym przypadku kończy się albo idealną liczbą, albo zwraca błąd Error („Przykro mi, liczba zbyt duża”). Blok catch nigdy nie zostanie uruchomiony.
Problem polega na tym, że foo zwraca obietnicę przekazaną od canRejectOrReturn. Dlatego rozwiązanie funkcji foo staje się rozwiązaniem dla canRejectOrReturn. W tym przypadku kod składa się tylko z dwóch linii:
try {
const promise = canRejectOrReturn();
return promise;
}A oto co będzie, jeśli użyjesz jednocześnie await i return:
async function foo() {
try {
return await canRejectOrReturn();
} catch (e) {
return 'wycatchowany błąd';
}
}W powyższym kodzie foo zakończy się zarówno z perfect number, jak i z error caught. Nie będzie tutaj żadnych awarii. Jednak foo zakończy się z canRejectOrReturn, a nie z undefined. Upewnijmy się o tym, usuwając linię return await canRejectOrReturn():
try {
const value = await canRejectOrReturn();
return value;
}
// …Powszechne błędy i pułapki
W niektórych przypadkach użycie Async/Await może prowadzić do błędów.
Zapomniany await
Tego zdarza się dość często — przed obietnicą brakuje kluczowego słowa await:
async function foo() {
try {
canRejectOrReturn();
} catch (e) {
return 'caught';
}
}W kodzie, jak widać, nie ma ani await, ani return. Dlatego foo zawsze kończy się jako undefined bez opóźnienia jedną sekundą. Jednak obietnica będzie wykonywana. Jeśli jednak wystąpi błąd lub zostanie odrzucona, w takim przypadku wywołane zostanie UnhandledPromiseRejectionWarning.
Funkcje Async w wywołaniach zwrotnych
Funkcje Async są dość często używane w .map lub .filter jako wywołania zwrotne. Przykładem może być funkcja fetchPublicReposCount(username), która zwraca liczbę otwartych repozytoriów na GitHubie. Załóżmy, że mamy trzech użytkowników, których dane są nam potrzebne. Oto kod do tego zadania:
const url = 'https://api.github.com/users';
// Funkcja pomocnicza do pobierania liczby repozytoriów
const fetchPublicReposCount = async (username) => {
const response = await fetch(`${url}/${username}`);
const json = await response.json();
return json['public_repos'];
}Potrzebujemy kont ArfatSalman, octocat, norvig. W tym przypadku wykonujemy:
const users = [
'ArfatSalman',
'octocat',
'norvig'
];
const counts = users.map(async username => {
const count = await fetchPublicReposCount(username);
return count;
});Należy zwrócić uwagę na Await w wywołaniu zwrotnym .map. Tutaj counts to tablica obietnic, a .map to anonimowe wywołanie zwrotne dla każdego określonego użytkownika.
Nadmierna sekwencyjna obsługa await
Jako przykład weźmy taki kod:
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;
}Tutaj do zmiennej count przypisywana jest liczba repozytoriów, a następnie ta liczba dodawana jest do tablicy counts. Problemem kodu jest to, że dopóki dane pierwszego użytkownika nie zostaną pobrane z serwera, wszyscy pozostali użytkownicy będą w trybie oczekiwania. W ten sposób w danym momencie przetwarzany jest tylko jeden użytkownik.
Na przykład, jeśli na przetworzenie jednego użytkownika potrzeba około 300 ms, to dla wszystkich użytkowników już wychodzi sekunda; czas przetwarzania jest liniowo zależny od liczby użytkowników. Jednak ponieważ pobieranie liczby repozytoriów nie zależy od siebie nawzajem, procesy można rozdzielić. W tym celu potrzebna jest praca z .map i 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 przyjmuje tablicę obietnic, zwracając obietnicę. Ta obietnica kończy się po zakończeniu wszystkich obietnic w tablicy lub przy pierwszym odrzuceniu. Może się zdarzyć, że nie wszystkie uruchomią się jednocześnie — aby zapewnić ich równoczesne uruchomienie, można użyć p-map.
Podsumowanie
Funkcje asynchroniczne stają się coraz ważniejsze dla programowania. Aby efektywnie korzystać z funkcji asynchronicznych, warto skorzystać z . Programista JavaScript powinien dobrze je rozumieć.
Skillbox poleca:
- Praktyczny kurs .
- Kurs online .
- Dwuletni praktyczny kurs .
Źródło: habr.com
