Polub bloga na fejsie!

Składnia async/await – nowy sposób na Promisy

13

Składnia async/await jest (na tę chwilę) częścią specyfikacji ECMAScript 2017. Ostatnio, w moim prywatnym projekcie, nad którym w pocie czoła pracuję (niebawem napiszę o nim na blogu), całkiem sporo tej składni używam. Postanowiłem więc napisać na jej temat parę słów ponieważ, wydaje mi się, że nie jest ona jeszcze powszechnie stosowana. Zatem zapraszam do lektury!

Jak to się robiło dotychczas

Do tej pory, do obsługi wywołań asynchronicznych w JavaScript wykorzystywaliśmy callbacki oraz obiekty „promise”. Co do callbacków w tym kontekście to pozwólcie, że spuścimy tutaj zasłonę milczenia (o „callback hell” pisałem już zresztą na blogu)… Skupmy się zatem na „promisach” i wyobraźmy sobie taki przykładowy kod (w przykładach będę stosować składnię „ES6+”):

Powyższy przykład jest raczej prosty: losujemy dwa timeouty, które przekazujemy jako parametry funkcji setTimeout. Dzięki temu operacja asynchroniczna zakończy się losowo: sukcesem lub porażką.

Powyższa implementacja nie jest tutaj aż tak istotna. Bardziej interesujący jest przykład użycia funkcji asyncAction:

Napewno dobrze już to znasz. Obiekt Promise zwracany przez funkcję asyncAction posiada metodę then pozwalającą na obsługę operacji asynchronicznej zakończonej powodzeniem (w momencie wywołania resolve() w funkcji asyncAction. Posiada on też metodę catch(), która pozwala obsłużyć niepowodzenie operacji asynchronicznej. W przykładzie, w obu przypadkach w konsoli wyświetlany jest odpowiedni komunikat.

Ok, to tyle jeśli chodzi o punkt wyjścia do pokazania jak działa składnia async/await. Na ten temat więcej tym poniżej.

Składnia async/await

Generalnie, składnia async/await to tylko inny sposób zapisu. „Pod spodem” nadal wykorzystywane są „promisy”. Pozwala ona jednak na pisanie kodu w sposób bardziej synchroniczny.

Aby wyjaśnić o co chodzi myślę, że najlepiej będzie pokazać przykład. Spójrzmy na przepisany od nowa kod funkcji doWork.

Objaśnienie przykładu

Pierwsze co się rzuca w oczy to słowo async przed definicją funkcji doWork(). Słowo to powoduje, że funkcja doWork jest od teraz asynchroniczna i co do zasady zwraca ona obiekt Promise (dzieje się to „pod spodem”). Kiedy następuje wywołanie resolve() w takim przypadku? W momencie użycia słowa kluczowego return! Czyli wywołanie return 'success' jest tożsame z wywołaniem resolve('success').

Jak możesz zauważyć, implementacja funkcji doWork dość mocno się zmieniła. Mamy tutaj teraz blok try ... catch ale o tym za chwilę. Najpierw spójrz na linię trzecią. Do zmiennej data przypisujemy wynik wywołania funkcji asyncAction ale jest on jeszcze poprzedzony słowem kluczowym await. Oznacza ono tyle, że wynik działania funkcji asyncAction jest asynchroniczny.

Najciekawsze znajdziesz w kolejnej linii. Niby zwykłe wywołanie console.log(). Jednak zauważ, że za jej pomocą wyświetlana jest wartość zmiennej data, która przecież może jeszcze nie istnieć w tym momencie! I to jest właśnie cała „magia”: jeżeli przy wywołaniu funkcji asynchronicznej (czyli, jak już powiedzieliśmy takiej, która zwraca Promise) użyjemy słowa await to kod, który zależy od wyniku działania tej funkcji również staje się asynchroniczny. Można powiedzieć, że „zaczeka” on na ten wynik i zostanie wykonany dopiero w momencie zakończenia operacji asynchronicznej.

Kolejna sprawa to blok try ... catch. Otóż przy wywołaniu funkcji asynchronicznej poprzedzonej słowem await dostajemy wynik „resolwowania” obiektu Promise. Jak więc obsłużyć przypadek wywołania metody reject? Ano właśnie owijając wywołanie z await blokiem try ... catch. Jak możesz wywnioskować z powyższego przykładu, zmienna przekazana podczas wywołania metody reject jest później dostępna jako parametr dyrektywy catch. Łatwo więc się do niego dostać.

P.S. O ile w naszym przypadku zwracanie obiektu Promise z metody doWork nie jest potrzebne (nie wykorzystujemy wyniku wywołania tej funkcji) o tyle słowo async przed definicją tej funkcji jest wymagane jeśli chce się w niej skorzystać ze słowa kluczowego await!

Czy da się tego używać wszędzie?

Niestety nie bardzo… Składnia async/await zbudowana jest w oparciu o obiekty Promise. Nie da się jej używać ze zwykłymi funkcjami wywołania zwrotnego.

Spójrz na przykład na naszą funkcję asyncAction. Funkcje resolve oraz reject są tam wywoływane wewnątrz callbacków przekazywanych do funkcji setTimeout. Jest to problem ponieważ, jak już wspomniałem, przy składni async/await „resolwowanie” następuje w momencie zwrócenia wartości z funkcji. Czegoś takiego przecież nie zrobimy:

… ponieważ to co jest zwracane w funkcji callback jest „połykane” przez funkcję setTimeout. Co innego gdyby zwracała ona obiekt Promise z tym wynikiem… ale nie zwraca (zamiast tego dostajemy ID timera).

Podsumowanie

Składnia async/await to, jak widzisz, nie jest żadna magia. Jest to po prostu inny sposób pracy z obiektami Promise, dzięki któremu tworzony przez nas kod wygląda bardziej „synchronicznie”. Niewątpliwą zaletą tego podejścia jest też sposób obsługi błędów, który jest spójny z tym w jaki sposób wyłapuje się błędy dla operacji synchronicznych. Często pozwala też nieco „spłaszczyć” łańcuch wywołań operacji asynchronicznych – na przykład jeśli wynik jednej operacji asynchronicznej chcemy przekazać do kolejnej to zamiast zagnieżdżać metody then, wystarczy „poczekać” na wynik pierwszej operacji za pomocą await i przekazać go do kolejnej operacji asynchronicznej (na przykład w następnej linii).

W związku z powyższym, wydaje mi się, że warto to podejście wypróbować ponieważ, kod z jego użyciem wygląda „czyściej” i bardziej logicznie.

P.S. Kody pokazane powyżej dostępne są do przetestowania na githubie.

REACT, REDUX, REACT-ROUTER - KURSY ON-LINE

Chcesz od podstaw poznać tajniki React, Redux oraz react-router? Zapraszam do moich szkoleń on-line:

Przejdź do szkoleń

Uwaga! Obecnie trwa przedsprzedaż kursów - premiera 1 sierpnia 2017!

  • Patryk

    W pierwszym przykładzie składni ES6 są literówki w słowie Math (Match.random()).

  • W sumie async/await bardzo fajnie się tłumaczy na przykładzie analogii do generatorów – IMO bardzo fajne na zastanowienie się, jak to może wyglądać od spodu.

    • myślę, że masz rację – korzysta się z tego podobnie… wpis pisałem jednak też z myślą o juniorach, którzy pewnie wcześniej natrafią na swojej drodze na async/await niż na generatory

  • Maciej Cąderek

    Co do akapitu „Czy da się tego używać wszędzie?” – nie do końca rozumiem zarzut – await służy do konsumowania promisa (lub zwykłej wartości) i tyle, nie dziwne, że nie da się zrobić bezsensownych rzeczy. A asyncAction nadal może stosować await, choć to bez sensu trochę:

    możesz też po prostu zwrócić promisa jak w oryginalnej funkcji:

    Piszesz:

    kod, który zależy od wyniku działania tej funkcji również staje się asynchroniczny

    no nie – kod synchroniczny to kod synchroniczny (no może console.log to akurat słaby przykład kodu synchronicznego, bo może być implementowany różnie), jego wykonanie jest odsunięte w czasie do zresolvowania promisa ale nadal to kod blokujący główny wątek podczas swojego wykonania.

    • to nie żaden zarzut tylko stwierdzenie, że to właśnie bez sensu, bo async/await to nic innego jak inny sposób zapisu Promisów… no ale ja wiem, że czasem trzeba się trochę powymądrzać – ja mam od tego bloga, a Ty komentarze 😛

      • Maciej Cąderek

        A ok, czyli nie ma tam czegoś czego nie zrozumiałem, ot jałowy akapit (dla mnie! niekoniecznie dla innych) 😉 Co do „wymądrzania” – jak tak to odbierasz to ok, widać mnie bardziej zależy na jakości bloga niż Tobie, a szkoda, bo przy marnej jakości polskich materiałów o JSie Twój kontent się wyróżnia.

        • Czytają to ludzie na bardzo różnym poziomie wiedzy i coś co dla Ciebie jest oczywiste nie musi być takie dla innych. Dlatego też dodałem ten akapit aby uprzedzić pytania w stylu „a jak to zastosować do callbacków, nic nie napisałeś o tym”… A Twój komentarz odebrałem za „wymądrzanie” bo był on, w moim odczuciu, w dość czepialskim tonie. Mimo wszystko dzięki za komentarze i cieszę się, że uważasz to co robię za wartościowe. 😉 Pozdrawiam

  • Hej. A możesz powiedzieć w jakim stopniu radzi sobie z tym Babel?
    Bo dla mnie to fajnie super wygląda, ale chyba trochę za wcześnie? Do czego to się da transpilować?
    To by super wyglądało z rxJs na subscribie. Tak wiem że jest „toPromise”, ale „normalnie” też by było spoko.

Google Analytics Alternative