Obiecuję, że po przeczytaniu tego wpisu, Wasz kod asynchroniczny stanie się bardziej czytelny. Skąd taka pewność siebie? A no, społeczność JavaScript od dwóch lat zachęca do jak najczęstszego składnia obietnic. Spokojnie, to nadal powtórka przed ReactJS, a nie zapowiedź mojego startu w plebistycie na najlepszego blog front-endowy. Na to przyjdzie czas wiosną 2018 roku #DSP ;).
Pamiętam swoje początki z kodem asynchronicznym. Kiedy wreszcie zrozumiałem mechanikę otrzymywania danych z opóźnieniem, szybko natrafiłem na kolejny problem. Nierzadko musiałem wykorzystać otrzymaną z API wartość do wykonania kolejnego zapytania do serwera. Liczba linijek kodu zaczynała rosnąć, a mi co raz ciężej nad tym wszystkim zapanować. Zacząłem zastanawiać się czy czytelny AJAX to swego rodzaju oksymoron. Niedługo później udałem się na swoją pierwszą konferencję, tam na wykładzie nt. programowania asynchronicznego ujrzałem następujący slajd.
Piramida zagłady, callbackowe piekło – programiści potrafią być bardzo kreatywni opisując kilka linijek zbędnego kodu ;). Jak się okazało, nie byłem jedyną osobą, która miała problem z organizacją AJAXa. Na szczęście, dzięki Obietnicom, nie jest to sytuacja bez wyjścia.
Zagadnienia, które pozwolą nam na zrozumienie tego jak dobrze złożone obietnice mogą ustrzec nas przed piekłem:
- Czym są Obietnice?
- Procedury obsługi:
.then()
icatch()
- Łańcuchownie Obietnic
Promise.all()
- Na co uważać stosując Obietnice?
- Callbacki vs Obietnice – porównanie.
W ramach nagrody za poradzenie sobie z teorią, na końcu czeka na Was kilka zadań pozwalających przećwiczyć omówiony materiał.
Czym są Obietnice?
Obietnice to specjalny obiekt. Jak to z obiektami bywa, można je utworzyć za pomocą konstruktora. Konstruktor Obietnicy jako parametr przyjmuje funkcję o dwóch argumentach: resolve
i reject
. Aby spełnić obietnicę, wewnątrz tejże funkcji, wywołujemy resolve(value)
. value
to wartość, którą chcemy przekazać do procedury obsługi. Jeżeli chcemy odrzucić obietnicę, to wywołujemy reject(error)
. Jako error możemy przekazać dowolną wartość podobnie jak w przypadku resolve. Jednakże dobrą praktyką jest zwracanie obiektu błędu lub chociaż wiadomości opisującej powód odrzucenia obietnicy. Przed spełnieniem lub odrzuceniem, obietnica jest w stanie oczekującym.
Obietnice mają dwie wewnętrzne właściwości: stan i wartość zwrotną. Stan Obietnicy początkowo jest ustawiony na ‚oczekująca’ (pending). W przypadku wywołania resolve()
, stan zmienia się na ‚wywiązana’ (fulfilled). Analogicznie, reject()
sprawia, że mamy do czynienia z ‚odrzuconą’ (rejected Obietnicą. Wartość zwrotna to wartość zwracana przez Obietnicę, czyli argument przekazany do dwóch wcześniej wymienionych metod.
Jeżeli wykorzystujemy wewnętrzną metodę resolve(value)
, która oznacza pomyślne wykonanie zadania, to ustawiamy właściwości:
- Stan na ‚wywiązana’
-
Wartośc zwrotna na
value
Jeżeli wykorzystujemy wewnętrzną metodę reject(error)
, która oznacza wystapienie błędu podczas wykonywania zadania, to ustawiamy właściwości:
- Stan na ‚odrzucona’
-
Wartość zwrotna na
error
Utworzenie obietnicy to dopiero początek, musimy ją jeszcze wykonać. Aby to zrobić, wystarczy wywołać ją jak każdą funkcję. W następstwie wywołania obietnicy, otrzymujemy dostep do procedury obsługi .then()
. Przyjmuje ona dwa argumenty, każdy z nich to funkcja przechwytująca wartość przekazaną przez Obietnicę. Pierwsza z nich zostanie wywołana w przypadku wywiązania się z obietnicy (zastosowanie resolve()
), druga z nich w przypadku błędu (zastosowanie reject()
).
Aby zwiększyć czytelność kodu, możemy wykorzystać drugą procedurę obsługi, .catch. Zastępuje ona drugą funkcję przekazywaną do .then() – tę, która służy do przechytywania błędu. To follow-up do bloku try/catch
.
Kod funkcji przekazanej do obietnicy, jest wykonywany automatycznie i natychmiastowo, w momencie utworzenia nowej obietnicy za pomocą konstruktora new Promise
.
Wywołanie resolve()
bądź reject()
jest równoznaczne z zastosowaniem słowa kluczowego return
w normalnej funkcji. Silnik zwraca wartość i przechodzi do wykonywania kolejnej funkcji ze stosu. Stąd nie ma możliwości wywiązania i odrzucenia tej samej Obietnicy.
Warto wiedzieć: mimo że możemy przekazać do reject jakąkolwiek wartość, to preferowane jest zwracanie obiektów połączonych prototypowo z Error
.
Procedury obsługi: .then() i catch()
Działanie procedur obsługi Obietnicy świetnie obrazuje poniższy przykład. Za jego pomocą możemy dołączyć dowolny skrypt do nagłówka strony.
Dopóki Obietnica jest w stanie oczekującym, to procedury .then()
i .catch()
czekają na wartość zwrtoną. Po jej przekazaniu, dochodzi do wywołania jednej z procedur.
Tak to wygląda w przypadku kodu asynchronicznego. Nic nie stoi na przeszkodzie, aby Obietnica obsługiwała synchroniczną funkcję. Tym razem procedury wykonają się natychmiastowo.
Tutaj warto zaznaczyć, że same funkcje przekazane do .then()/.catch()
zawsze są wykonywane asynchronicznie. Trafiają one do wewnętrznej kolejki wykonywania funkcji. Silnik JS pobiera kolejne funkcji z kolejki i wykonuje je po zakończeniu działania obecnie wykonywanego kodu. Działa to na podobnej zasadzie jak setTimeout(…, 0).
Jedną z niebywałych zalet obietnic jest możliwość wykorzystywania return
– w ten sposób rezygnujemy z niepożądanego wykorzystywania efektów ubocznych.
Co możemy zwrócić wewnątrz .then()
?
- Kolejną Obietnicę
- Wartość zwrotną
- Wyrzucić błąd za pomocą throw
1. Zwracanie Obietnicy.
2. Zwracanie wartości.
Nie jesteśmy ograniczeni do zwrócenia obietnicy, jeżeli mamy dostęp do danych bez wykonywania zapytania – zróbmy to.
3. Wyrzucenie błędu za pomocą throw
.
W ten sposób możemy wyrzucić błąd z wartością synchroniczną. Spowoduje to przechywycenie błędu przez metodę .catch()
.
Łańcuchownie Obietnic
Wspomniana w wstępie piramida zagłady pojawia się w przypadku ciągłego przekazywania wartości zwrotnej jednej funkcji asynchronicznej do kolejnej. W przypadku Obietnic możemy utworzyć sekwencję .then()
i .catch()
o dowolnej długości.
Jeżeli obietnica zostanie wykonana, to przechodzimy do kolejnego .then()
. W przypadku błędu przechodzimy do najbliższego .catch()
.
Promise.all()
Przypuśćmy, że zależy nam na wywołaniu kilku Obietnic, jedna po drugiej. Nastepnie chcemy przeanalizować czy wszystko poszło zgodnie z planem. Najbardziej intuicyjnym rozwiązaniem byłoby obsłużenie wartości przechowywanych w tablicy za pomocą forEach
.
W takiej sytuacji nasza funkcja zwraca undefined. Gdybyśmy dodali return
w forEach, nic by się nie zmieniło. Kod wewnątrz forEach to zwykły callback, ma swój własny zakres.
Co nas interesuje to return Promise.all()
. W ten sposób zgodnie z oczekiwaniami zareagujemy na wywiązanie/zerwanie wszystkich obietnic.
Jeżeli chcemy jednocześnie wykonać kilka obietnic i zareagować na to czy wszystkie zostały wykonane, to powinniśmy posłużyć się Promise.all()
. Metoda ta zwraca Obietnicę tylko jeżeli wszystkie Obietnice przekazane w parametrze (jako tablica, stąd zastosowanie .map()
zamiast .forEach()
) zostały wywiązane. Jeżeli jakakolwiek z Obietnic zostanie odrzuca, wywoła się procedura .catch()
.
Na co uważać stosując Obietnice?
Obietnice są czymś co ma uchronić nas od callbackowego piekła, a nie służyć za nakładkę składniową, z której nie wynika żadna poprawa w czytelności kodu.
Aby ustrzec się przed takim kodem, stosujmy łańcuchowanie i kompozycję Obietnic.
Warto pamiętać o umieszczaniu chociażby jednej procedury .catch()
. Bez tej metody nie dość, że nie przechwycimy żadnego błędu, to na dodatek nie będziemy świadomi ich wystąpienia – nie wyswietlą się nawet w konsoli.
Callbacki vs Obietnice – porównanie.
Okej, skoro udało nam się rozbudować wiedzę o Obietnicach to sensownym podsumowaniem byłoby porównanie czym tak naprawdę różnią się od callbacków.
Callbacki:
Musimy mieć gotowy callback w momencie wywoływania kodu asynchronicznego. Innymi słowy, musimy wiedzieć co zrobić z wynikami przed przystąpieniem do działania.
Nasz kod asynchroniczny może obsługiwać tylko jeden callback.
Obietnice:
Obietnice pozwalają na kodowanie czynności w naturalnym porzadku. Pierw wywołujemy funkcję wykonującą zapytanie. Następnie, rezultatem zajmujemy się wewnątrz procedur obsługi .then()
i catch()
. Możemy wywołać .then()
na jednej obietnicy ile razy chcemy – taki zabieg nazywamy łańcuchowaniem Obietnic. Jeżeli zależy nam na zbiorczej obsłudze kilku zapytań, możemy wykorzystać Promise.all()
.
Podsumowanie
Tyle na temat Obietnic. Udało nam się przerobić kolejny naprawdę istotny element ES6. Początkowo wydawało mi się, że temat jest naprawdę skomplikowany i trudno było mi się w tym wszystkim połapać. Mam nadzieję, że Wy nie macie takiego wrażenia po przeczytaniu tego wpisu.
Zachęcam do śledzenia bloga na facebooku. Zapraszam do wspólnego ćwierkania na Twitterze.
W przyszłym tygodniu ostatni (!) wpis omawiający nowinki z ES6, tym razem zajmiemy się modułami.
Zadania praktyczne
Każdy z odnośników prowadzi do edytora online z treścią zadania i przygotowanym środowiskiem. Poćwicz, co to by zdobyta wiedza nie uleciała w eter.
Źródła
- JavaScript Promises 101 – Ire Aderinokun, Bits of Code
- Promise Basics – Ilya Kantor, JavaScript.info
- We have a problem with Promises – Nolan Lawson, PouchDB
- Promises in AngularJS, Explained as a Cartoon – Andy Shora
Zdjęcie tytułowe autorstwa: rawpixel.com