Funkcje async i słowo kluczowe await
Wideo towarzyszące
Wideo pojawi się tu kiedy je nagramFunkcje async
Funkcje async są relatywnie nowym dodatkiem do języka JavaScript. Definiujemy je jak zwykłe, synchroniczne funkcje, jednak podstawowa różnica między nimi jest taka, że funkcje asynchroniczne zawsze zwracają obiekty Promise. Nawet jeżeli dane przez nie zwracane dostępne są natychmiast, zostaną one opakowane w Promise.
Ostatnio sobie o tym nie powiedzieliśmy, ale teraz jest po temu dobry moment: nawet jeżeli Promise jest w stanie resolved, nie da się z niej bezpośrednio wydobyć przechowanych w środku danych. Jedyny sposób, żeby je odczytać w naszym kodzie, to przekazać je do funkcji callbackowej, która będzie uruchomiona przez pętlę zdarzeń z kolejki mikrotasków. Jest to celowa decyzja projektantów języka.
Poniżej przykładowy kod definiujący funkcję asynchroniczną getNumberAsync. Jak widać, jedyna różnica pomiędzy definicją funkcji synchronicznej to dodanie słowa async przed słowem function.
| |
Wykonanie powyższego kodu da nam następujący wynik:
42
Promise { 42 }
42Co prawda Node.js drukuje nam zawartość danych przechowanych w zwróconym obiekcie Promise, ale niech to nas nie zwiedzie - nasz kod może dostać się do przechowanej w środku liczby tylko w trybie asynchronicznym.
Funkcje arrow functions też mogą być asynchroniczne i rządzą się tymi samymi prawami, co funkcje definiowane z wykorzystaniem słowa kluczowego function. Poniżej przykład definicji asynchronicznej arrow function.
| |
Uruchom powyższy kod i sprawdź, czy działa podobnie jak kod z poprzedniego pliku.
Słowo kluczowe await
Słowo kluczowe await jest ściśle powiązane z funkcjami asynchronicznymi. Służy ono do pobierania wartości z Promise, także z tych zwracanych przez funkcje asynchroniczne.
Zaraz, zaraz, przed chwilą mówiłem, że wartości z Promise możemy dostać tylko w callbackach, w trybie asynchronicznym, a teraz istnieje jakaś inna metoda? I tak, i nie. Żeby to dokładnie wytłumaczyć weźmy na warsztat kolejny plik z kodem:
| |
Po uruchomieniu tego programu dostajemy następujący wydruk:
Module start
function start
getting number
Module end
function end, got number: 42Nie zdziwię się, jeżeli po przeczytaniu kodu spodziewałxś się trochę innego efektu wykonania. O ile w poprzedniej lekcji dość łatwo było wskazać, które fragmenty kodu wykonają się synchronicznie, a które nie, o tyle teraz ta linia zaczyna się trochę zacierać.
W powyższym przykładzie podczas wykonania kodu synchronicznie wykonały się linie 1-3 i 6 oraz 12-13. Linia 8 wykonała się asynchronicznie. Linia 7 jest natomiast trochę podchwytliwa. Część znajdująca się na prawo od słowa await została wykonana synchronicznie - wnioskujemy to po tym, gdzie pojawia się w wydruku tekst “getting number”. Natomiast przypisanie zwróconej wartości do zmiennej number już wydarzyło się asynchronicznie w ramach callbacku. Ale callbacku do czego? Przecież nie zdefiniowaliśmy żadnej dodatkowej funkcji, która mogłaby być wykonana jako callback!
W momencie wykonywania wyrażenia await funkcja waiting kończy swoje działanie. Zwraca wtedy Promise w stanie pending i zapisuje w pamięci informacje, w którym miejscu powrócić do wykonywania, kiedy Promise zwrócony z funkcji getNumber przejdzie w stan resolved i podpina się jako callback do tego Promise.
Jeżeli w dotychczasowych przygodach z programowaniem nie zdarzyło Ci się natknąć na funkcje, które zachowują informacje na temat tego, gdzie ostatnio zakończyły wykonanie i potrafią kontynuować wykonanie od tego miejsca, to może Ci zająć chwilę oswojenie się z tym konceptem.
Jeśli nie jesteś przekonany, że przypisanie wartości do zmiennej number odbywa się asynchronicznie, spróbuj uruchomić tę zmodyfikowaną wersję kodu:
| |
Wykonanie programu zakończy się błędem, ponieważ próbowaliśmy odczytać wartość zmiennej przed jej inicjalizacją. A to dlatego, że zmienna byłaby zainicjalizowana dopiero po zakończeniu wykonania kodu synchronicznego, w callbacku podpiętego do Promise z wywołania funkcji getNumber.
Stos wykonania
W poprzedniej lekcji przedstawiłem krótko temat stosu kontekstów wykonania funkcji. W innych językach programowania też często pojawia się taki koncept, zazwyczaj nazywany stosem wywołań (ang. call stack). W JavaScript też często używamy tej nazwy, ponieważ call stack jakoś łatwiej rzucić w rozmowie niż execution context stack.
Jeżeli wykonałeś poprzedni kod albo natknąłeś się na jakiś inny błąd podczas swojej własnej pracy w JavaScript, to już widziałeś na pewno nie jeden wydruk ze stosu wykonania. Jeżeli chcemy po prostu podejrzeć stos wywołania w naszym programie, możemy użyć metody console.trace.
| |
Na moim komputerze na używanej przeze mnie wersji Node.js ten program podczas wykonania drukuje następujący zapis stosu wykonania:
Trace
at inner (file:///Users/kleindan/Projects/pzaw-code/lekcja20/03_callstack/demo.js:8:12)
at outer (file:///Users/kleindan/Projects/pzaw-code/lekcja20/03_callstack/demo.js:4:4)
at file:///Users/kleindan/Projects/pzaw-code/lekcja20/03_callstack/demo.js:1:1
at ModuleJob.run (node:internal/modules/esm/module_job:274:25)
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:644:26)
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:98:5)Na górze wydruku znajduje się obecnie wykonywana funkcja, a pod nią funkcja, która ją wywołała. Poniżej zostaje nam już tylko poziom wykonywania pliku z kodem i wewnętrzne funkcje Node.js.
Uzbrojeni w to narzędzie, możemy jeszcze raz wrócić do poprzedniego przykładu i ponownie pokazać, że funkcje asynchroniczne mogą być wykonywane zarówno w kontekście synchronicznym, jak i poza nim:
| |
Wydruk z wykonania programu pokazuje dwa stosy wywołań. Drugi z nich jest znacznie krótszy i pokazuje, że faktycznie funkcja waiting jest wykonana już poza kontekstem wykonywania pliku z kodem.
Trace
at waiting (file:///Users/kleindan/Projects/pzaw-code/lekcja20/03_callstack/await.js:4:12)
at file:///Users/kleindan/Projects/pzaw-code/lekcja20/03_callstack/await.js:1:1
at ModuleJob.run (node:internal/modules/esm/module_job:274:25)
at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:644:26)
at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:98:5)
Trace
at waiting (file:///Users/kleindan/Projects/pzaw-code/lekcja20/03_callstack/await.js:6:12)Warto mieć to na uwadze w przyszłości: w przypadku korzystania z funkcji asynchronicznych musimy mieć świadomość, że użycie await odłoży wykonanie reszty funkcji i pozwoli na wykonanie reszty synchronicznego kodu w międzyczasie. Co oznacza, że nie możemy polegać na wykonaniu całości funkcji asynchronicznej w naszym kodzie synchronicznym.
Await a funkcje synchroniczne
Await w funkcjach synchronicznych jest niedozwolone. Możesz się o tym łatwo przekonać próbując wstawić słowo kluczowe await w dowolnej funkcji synchronicznej i spróbować uruchomić kod - program zwróci błąd już na etapie kompilowania funkcji.
Natomiast JavaScript pozwala na użycie await w ramach pobierania wyniku z funkcji synchronicznych. W takim wypadku JS po prostu opakowuje wynik w obiekt Promise, reszta zachowania programu pozostaje bez zmian.
Obsługa błędów z wykorzystaniem await
Na ostatniej lekcji mówiliśmy, że Promise pozwalają na obsługę błędów na różne sposoby, albo poprzez podanie odpowiedniego callbacku do metody then, albo poprzez podpięcie callbacku do całego łańcucha Promise przy pomocy metody catch.
W przypadku stosowania await w wypadku przejścia Promise w stan rejected zostanie rzucony wyjątek, który powinien być obsłużony normalnym blokiem try ... catch.
Zadania praktyczne
W towarzyszącym repozytorium w folderze z kodem do tej lekcji znajdują się dodatkowe przykłady użycia await, funkcji asynchronicznych i Promises. Spróbuj uruchomić te przykłady i zrozumieć, co robią i jak dokładnie działają.