Async/await – podstawy języka JavaScript

3111
views
Async/await - podstawy języka JavaScript

W poprzednim artykule z cyklu podstaw języka JavaScript, pokazałem jak zastosowanie obietnic pozytywnie wpłynęło na pracę z kodem asynchronicznym i w jaki sposób pozbyć się piekła wywołań zwrotnych (callback hell). Istnieje jednak możliwość zapisu kodu asynchronicznego w sposób jeszcze bardziej czytelny.

Załóżmy że chcemy napisać program, który połączy się z zewnętrznym API w celu pobrania nazwy miasta i na jej podstawie z innego zewnętrznego API wczyta dane pogodowe. W uproszczeniu kod takiego programu mógłby wyglądać następująco:

Ponieważ jednak pracujemy z kodem asynchronicznym to w takim przypadku użyjemy obietnic i uzyskamy następujący zapis:

https://jsfiddle.net/q7dmoas0/

Mamy tutaj do czynienia z typowym asynchronicznym łańcuchem zależności. Potrzebujemy posiadać nazwę miasta, aby na jego podstawie pobrać informacje pogodowe.

Jak widać użycie obietnic w porównaniu z pierwszym zapisem niesie ze sobą pewną nadmiarowość, ale także i ograniczenia. Jeśli chcielibyśmy wypisać w ostatnim wywołaniu oprócz temperatury również nazwę miasta, musielibyśmy zapisać ją w dodatkowej zmiennej.

https://jsfiddle.net/ehboqb8f/

Czy nie było by idealnie, gdybyśmy mieli możliwość zapisu kodu asynchronicznego w taki sposób, aby był on tak samo czytelny jak kod synchroniczny z pierwszego przykładu?

Generatory

Jedną z możliwości poprawy czytelności jest zapis z użyciem generatorów, wprowadzonych do języka JavaScript od wersji ES6. W skrócie, generator to specjalny rodzaj funkcji (oznaczamy go za pomocą * umiejscowionej za słowem kluczowym function), której przebieg może być zatrzymywany i wznawiany z zachowaniem kontekstu (variable bindings). Wywołanie generatora nie wykonuje od razu poleceń w nim zawartych, zwraca natomiast obiekt iteratora. Następnie za pomocą metody next() wykonywany jest kod wewnątrz funkcji.

Zobaczmy na przykładzie w jaki sposób działa generator w JavaScript.

https://jsfiddle.net/hu6f72Le/

Przebieg powyższego skryptu jest następujący.

  1. Podczas przypisania funkcji do zmiennej var gen = foo(5) zwracany jest obiekt iteratora.
  2. Pierwsze wywołanie metody next() na obiekcie iteratora powoduje przypisanie wartości 5 do zmiennej x, zatrzymanie wykonywania funkcji na słowie kluczowym yield oraz zwróceniu wartości {value: 6, done: false}.
  3. Drugie wywołanie metody next() powoduje zatrzymanie wykonywania kodu na drugim słowie kluczowym yield i zwrócenie komunikatu ‚Hello’ w postaci {value: „Hello”, done: false}.
  4. Kolejne wykonanie metody next(), tym razem z przekazaną wartością 8, przypisuje przekazaną wartość w miejscu poprzedniego zatrzymania funkcji, czyli let y = yield ‚Hello’ (let y = 8), zwraca wynik z obliczeń return x + y (5 + 8) oraz informację o zakończeniu działania iteratora done: true. W konsoli ukazuje się nam wpis: {value: 13, done: true}.

Nie chciałbym jednak skupiać się w tym miejscu na szczegółowym opisie możliwości generatorów, a bardziej na ich połączeniu z obietnicami i zastosowaniu w pracy z kodem asynchronicznym. Tak więc, powracając do przykładu z początku artykułu użyjmy generatora, aby pobrać informacje pogodowe dla danego miasta.

https://jsfiddle.net/qct8s58o/

Funkcja *app() pokazuje siłę generatorów w pracy z kodem asynchronicznym. Porównując kod zawarty w tej funkcji z kodem synchronicznym z początku artykułu, zauważysz, że jest on niemal identyczny. To wielka zaleta stosowania generatorów w połączeniu z obietnicami. Jeśli chodzi o metodę start(), na tę chwilę nie zaprzątajmy sobie nią głowy. Jest ona szczegółem implementacyjnym, i na dobrą sprawę moglibyśmy w tym miejscu użyć bibiliotekę opakowującą np. promise.coroutine lub co.

Async/await

Po tym obszernym wstępie przejdźmy w końcu do omówienia async/await. Z wiedzą jaką posiadamy na temat generatorów, przesiadka na async/await będzie niezwykle prosta. Weźmy funkcję function *app() utworzoną wcześniej i zamieńmy function * na słowo kluczowe async function oraz yield na await.

https://jsfiddle.net/a7o19vgg/

Tak, to wszystko. Rezultat działania będzie ten sam co w przypadku generatora, zauważ jednak że nie mamy tutaj potrzeby użycia funkcji pomocniczej start(). Zamiast tego, JavaScript używa wbudowanych mechanizmów do obsługi tego typu wyrażenia.

Funkcja async zawsze zwraca obiekt Promise. Jeśli wewnątrz funkcji wystąpi błąd, obiekt Promise zostanie odrzucony (rejected), jeśli Promise zostanie spełniony (resolve), funkcja async zwróci wartość. Oprócz poprawy czytelności stosowanie async/await daje nam jeszcze kilka dodatkowych korzyści.

Obsługa błędów

Używając async/await mamy możliwość użycia bloku try/catch w celu przechwytywania błędów w kodzie asynchronicznym.

Aby w pełni obsłużyć możliwość przechwycenia błędów synchronicznych i asynchronicznych w kodzie z użyciem obiektów Promise, musimy zastosować następujące rozwiązanie.

Zobaczmy teraz jak może to wyglądać z użyciem async/await.

Obsługa wyrażeń warunkowych

Spójrzmy teraz na nieco bardziej skomplikowaną logikę aplikacji. Załóżmy że na podstawie danych otrzymanych z getSome() musimy podjąć decyzję czy wykonać jeszcze jedno zapytanie do API czy zwrócić otrzymaną wartość. Kod zapisany za pomocą obietnic wyglądałby następująco:

Znacznie bardziej czytelnie jest gdy użyjemy async/await.

Współbieżność z użyciem async/await

Spójrzmy na następujący przykład, w którym pobierzemy dwa razy nazwę miasta z zewnętrznego API.

https://jsfiddle.net/71n9n3tk/

Gdy wykonamy powyższy skrypt w konsoli w zakładce Network możemy zobaczyć że requesty do API wykonywane są w sposób sekwencyjny.

Sekwencyjne pobranie danych z API
Sekwencyjne pobranie danych z API

Zmieńmy zatem nasz kod w taki sposób, aby zapytania do API działały współbieżnie.

https://jsfiddle.net/vb23xvpm/

W pierwszej części skryptu jednocześnie uruchamiamy zapytania do zewnętrznego API za pomocą metody fetch(). Następnie oczekujemy na poprawne rozwiązanie obietnic z kroku pierwszego. Całość czeka tak długo, aż uda się rozwiązać obietnice i zwrócić wynik.

Współbieżne pobranie danych z API
Współbieżne pobranie danych z API

Alternatywnie możemy użyć metody Promise.all() podczas oczekiwania na rozwiązanie większej ilości obietnic. Wynik skryptu będzie taki sam jak poprzednio.

https://jsfiddle.net/6agnreme/

Podsumowanie

Składnia async/await pojawiła się w wersji ES8 (ECMAScript 2017) języka JavaScript, nareście wprowadzając intuicyjność do pracy z kodem asynchronicznym. Async/await jest wspierane przez wszystkie nowoczesne przeglądarki. Programiści Node mogą korzystać z nowej składni od wersji 8. Jeśli jednak na liście brakuje wsparcia dla środowiska którego używasz możesz skorzystać z jednego z narzędzi takich jak Babel lub biblioteka asyncawait.

Wsparcie dla async function
Wsparcie dla async function