Cyfroteka.pl

klikaj i czytaj online

Cyfro
Czytomierz
00799 010977 7459698 na godz. na dobę w sumie
Tajniki języka JavaScript. Asynchroniczność i wydajność - ebook/pdf
Tajniki języka JavaScript. Asynchroniczność i wydajność - ebook/pdf
Autor: Liczba stron: 240
Wydawca: Helion Język publikacji: polski
ISBN: 978-83-283-2181-6 Data wydania:
Lektor:
Kategoria: ebooki >> komputery i informatyka >> webmasterstwo >> javascript - programowanie
Porównaj ceny (książka, ebook (-20%), audiobook).
Istnieje wiele podręczników do nauki języka JavaScriptu. Większość z nich nie wyczerpuje trudniejszych i bardziej zaawansowanych zagadnień, których zrozumienie — choć wymaga wysiłku — jest warunkiem osiągnięcia prawdziwej biegłości w tym języku. JavaScript jest jednym z przystępniejszych języków programowania i można go używać, znając jedynie podstawy. Równocześnie jednak ten łatwy i zachęcający język zawiera wiele zaawansowanych, złożonych mechanizmów, których stosowanie w praktyce rozszerzy możliwości programisty w zadziwiający sposób. Szkoda, że tak niewielu programistów stara się dogłębnie poznać JavaScript!

Niniejsza książka jest częścią serii w całości poświęconej temu językowi. Założeniem autora było skupić się właśnie na tych głębszych aspektach języka JavaScript i wnikliwie je przeanalizować, a następnie, bazując na takich solidnych podstawach, pokazać praktyczne zastosowanie opisanych koncepcji. Owszem, JavaScript może być z powodzeniem wykorzystywany bez głębszej znajomości, jednak prawdziwą biegłość i kontrolę nad swoim kodem uzyskasz dopiero po zrozumieniu kilku trudniejszych koncepcji, z których część opisano w tej właśnie książce.

Dzięki tej książce:

Sprawdź, jakie zagadki kryje w sobie ten stary, dobry JavaScript!

Znajdź podobne książki

Darmowy fragment publikacji:

Tytuł oryginału: You Don t Know JS: Async Performance Tłumaczenie: Robert Górczyński ISBN: 978-83-283-2172-4 © 2016 Helion SA. Authorized Polish translation of the English edition You Don t Know JS: Async Performance ISBN 9781491904220 © 2015 Getify Solutions, Inc. This translation is published and sold by permission of O’Reilly Media, Inc., which owns or controls all rights to publish and sell the same. All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from the Publisher. Wszelkie prawa zastrzeżone. Nieautoryzowane rozpowszechnianie całości lub fragmentu niniejszej publikacji w jakiejkolwiek postaci jest zabronione. Wykonywanie kopii metodą kserograficzną, fotograficzną, a także kopiowanie książki na nośniku filmowym, magnetycznym lub innym powoduje naruszenie praw autorskich niniejszej publikacji. Wszystkie znaki występujące w tekście są zastrzeżonymi znakami firmowymi bądź towarowymi ich właścicieli. Autor oraz Wydawnictwo HELION dołożyli wszelkich starań, by zawarte w tej książce informacje były kompletne i rzetelne. Nie biorą jednak żadnej odpowiedzialności ani za ich wykorzystanie, ani za związane z tym ewentualne naruszenie praw patentowych lub autorskich. Autor oraz Wydawnictwo HELION nie ponoszą również żadnej odpowiedzialności za ewentualne szkody wynikłe z wykorzystania informacji zawartych w książce. Wydawnictwo HELION ul. Kościuszki 1c, 44-100 GLIWICE tel. 32 231 22 19, 32 230 98 63 e-mail: helion@helion.pl WWW: http://helion.pl (księgarnia internetowa, katalog książek) Drogi Czytelniku! Jeżeli chcesz ocenić tę książkę, zajrzyj pod adres http://helion.pl/user/opinie/tjsasy Możesz tam wpisać swoje uwagi, spostrzeżenia, recenzję. Printed in Poland. • Kup książkę • Poleć książkę • Oceń książkę • Księgarnia internetowa • Lubię to! » Nasza społeczność Spis treści Przedmowa .......................................................................................................................5 Wprowadzenie ..................................................................................................................7 1. Asynchroniczność — teraz i później .................................................................................11 12 14 16 21 28 30 32 Program we fragmentach Pętla zdarzeń Równoległe wykonywanie wątków Współbieżność Zadania Kolejność poleceń Podsumowanie 2. Wywołania zwrotne .........................................................................................................33 34 35 41 45 49 Kontynuacja Sekwencyjny mózg Kwestie zaufania Próba uratowania wywołań zwrotnych Podsumowanie 3. Obietnice .........................................................................................................................51 52 60 63 71 79 85 92 95 106 Czym jest obietnica? Określanie typu na podstawie then() Kwestie zaufania i obietnice Łańcuch przepływu kontroli Obsługa błędów Wzorce obietnic Powtórzenie wiadomości o API obietnic Ograniczenia obietnic Podsumowanie 4. Generatory ....................................................................................................................107 107 116 Złamanie zasady „działanie aż do zakończenia” Generowanie wartości przez generator 3 Poleć książkęKup książkę Asynchroniczna iteracja przez generatory Generatory plus obietnice Delegowanie generatora Współbieżność generatorów Koncepcja thunk Generatory istniejące przed wydaniem ES6 Podsumowanie 123 126 135 142 146 152 158 5. Wydajność programu .....................................................................................................159 160 165 167 170 Architektura wątków roboczych SIMD asm.js Podsumowanie 6. Testy wydajności i dostrajanie ........................................................................................173 173 177 180 183 184 191 192 Testy wydajności Kontekst ma znaczenie jsPerf.com Tworzenie dobrych testów Mikrowydajność Optymalizacja rekurencji ogonowej Podsumowanie A Biblioteka asynquence ...................................................................................................195 196 198 208 209 210 211 213 Projekt oparty na sekwencji API biblioteki asynquence Sekwencje wartości i błędu Obietnice i wywołania zwrotne Iterowane sekwencje Uruchamianie generatorów Podsumowanie B Zaawansowane wzorce asynchroniczności ......................................................................215 215 221 225 229 233 Iterowane sekwencje Zdarzenia reaktywne Współprogram generatora Koncepcja języka CSP Podsumowanie C Podziękowania ..............................................................................................................234 Skorowidz .....................................................................................................................237 4 (cid:95) Spis treści Poleć książkęKup książkę ROZDZIAŁ 3. Obietnice W rozdziale 2. przedstawiłem dwie główne kategorie wad użycia wywołań zwrotnych w celu wyraże- nia asynchroniczności w programie oraz zarządzania współbieżnością: brak sekwencyjności i brak zaufania. Skoro dokładnie poznałeś problemy, to możemy przejść do wzorców pozwalających na ich rozwiązanie. Problemem, którym chcę się zająć na początku, jest odwrócenie kontroli. Zaufanie to dość delikatna materia i bardzo łatwo można je stracić. Przypomnij sobie, jak kod zapewniający kontynuację działania programu umieściliśmy w funkcji wywołania zwrotnego przekazywanego później do innej części kodu (potencjalnie nawet do zewnętrz- nego) i trzymaliśmy kciuki, aby ten kod działał prawidłowo po uruchomieniu wywołania zwrotnego. Zdecydowaliśmy się na takie podejście, ponieważ chcieliśmy wyrazić: „Oto co się wydarzy później, po zakończeniu wykonywania bieżących kroków”. Czy można zrobić cokolwiek, aby nie dochodziło do wspomnianego odwrócenia kontroli? Czy nie lepiej zastosować zupełnie inne podejście — zamiast przekazywać na zewnątrz kod kontynu- ujący działanie programu, oczekiwać poinformowania o zakończeniu zadania, co pozwoli kodowi programu na określenie kolejnych kroków? Takie podejście jest określane mianem obietnicy. Obietnice zaczęły szturmem zdobywać świat JavaScript, ponieważ programiści i twórcy specyfi- kacji rozpaczliwie szukali sposobu na wyrwanie się z piekła wywołań zwrotnych. Tak naprawdę większość nowych asynchronicznych API dodawanych na platformie JavaScript i DOM została zbudowana na bazie obietnic. Dlatego też dobrym pomysłem będzie dokładne poznanie obietnic. Słowo „natychmiast” jest dość często używane w rozdziale i ogólnie rzecz biorąc, odnosi się do pewnej akcji rozwiązania obietnicy. Jednak w praktycznie wszystkich przypadkach, słowa „natychmiast” użyłem w odniesieniu do zachowania kolejki zadań (patrz rozdział 1.), a nie w sensie ściśle synchronicznego teraz. 51 Poleć książkęKup książkę Czym jest obietnica? Kiedy programista decyduje się na naukę nowej technologii bądź wzorca, to pierwszy jego krok zwykle można opisać jako „pokaż mi kod”. Całkiem naturalne jest szybkie rozpoczęcie pracy z nową technologią, a następnie stopniowe jej poznawanie w praktyce. Jednak okazuje się, że pewne kwestie mogą umknąć, gdy zapoznajesz się tylko z samym API. Obietnica to jedno z tych narzędzi, którego przeznaczenie i sposób użycia mogą być zupełnie oczy- wiste na podstawie analizy jego stosowania przez innych. Ograniczenie się do jedynie poznawania i używania API okaże się znacznie trudniejszym podejściem. Dlatego też zanim pokażę Ci przykład kodu opartego na obietnicy, najpierw chciałbym dokładnie wyjaśnić jej koncepcję. Mam nadzieję, że dzięki temu będziesz radził sobie znacznie lepiej podczas integracji obietnic we własnym kodzie asynchronicznym. Mając na uwadze powyższe kwestie, spójrzmy na dwie różne analogie pokazujące, czym jest obietnica. Przyszła wartość Wyobraź sobie następującą sytuację: udajesz się do restauracji typu fast food, zamawiasz cheese- burgera i płacisz za tę kanapkę. Przez złożenie i opłacenie zamówienia dokonałeś żądania wartości zwrotnej (cheeseburgera). W ten sposób zainicjowałeś transakcję. Jednak bardzo często się zdarza, że kanapka nie jest dostępna natychmiast. Sprzedawca daje Ci więc coś zamiast cheeseburgera: kartkę z numerem zamówienia. Ten numer zamówienia jest obietnicą, która gwarantuje Ci otrzymanie cheeseburgera w przyszłości. W ręku trzymasz rachunek i numer zamówienia. Ponieważ numer zamówienia przedstawia che- eseburgera w przyszłości, nie musisz się więcej martwić tą kanapką, poza tym, że jesteś głodny! Podczas oczekiwania na przyrządzenie kanapki możesz zajmować się innymi rzeczami, na przykład wysłać do przyjaciela SMS o treści: „Cześć, czy masz ochotę coś przekąsić? Wybrałem się na cheese- burgera”. Pozostajesz spokojny o cheeseburgera w przyszłości, choć jeszcze nie trzymasz go w ręku. Twój mózg pozwala na to, ponieważ numer zamówienia traktuje jako miejsce zarezerwowane dla cheese- burgera. Wspomniane miejsce zarezerwowane oznacza wartość niezależną od czasu. To jest przy- szła wartość. W pewnej chwili słyszysz: „Zamówienie numer 113!”. Podchodzisz do lady, trzymając numer za- mówienia, który następnie podajesz sprzedawcy i w zamian odbierasz swojego cheeseburgera. Innymi słowy, Twoja przyszła wartość jest już gotowa. Obietnicę wartości wymieniłeś na konkretną wartość. Poza przedstawioną powyżej sytuacją mogą się zdarzyć jeszcze inne. Na przykład po wywołaniu numeru zamówienia podchodzisz do lady, ale zamiast upragnionej kanapki sprzedawca z przy- krością informuje Cię: „Bardzo nam przykro, ale cheeseburgery się skończyły”. Pomijając frustrację klienta, w takiej sytuacji poznajesz jeszcze ważną charakterystykę przyszłej wartości — może wskazywać sukces lub niepowodzenie. 52 (cid:95) Rozdział 3. Obietnice Poleć książkęKup książkę Za każdym razem, gdy zamawiasz cheeseburgera, wiesz, że otrzymasz wskazaną kanapkę lub smutną wiadomość o jej braku i wówczas będziesz musiał zdecydować się na coś innego do jedzenia. W kodzie nie wszystko będzie tak proste, ponieważ użyty w powyższej metaforze numer za- mówienia może w ogóle nie zostać wywołany. W takim przypadku w nieskończoność pozostaje- my w stanie nierozwiązanym. Do tego tematu jeszcze powrócimy w dalszej części rozdziału. Wartości teraz i później Przedstawiona powyżej koncepcja może wydawać się zbyt abstrakcyjna, aby zastosować ją w kodzie. Przechodzimy więc do konkretów. Jednak zanim przejdę do przedstawienia obietnic, jeszcze na chwilę powrócimy do doskonale nam znanego kodu — wywołania zwrotne! — i zobaczymy, jak obsługuje wspomnianą przyszłą wartość. Podczas tworzenia kodu odpowiedzialnego za operacje na wartości, na przykład przeprowadzającego operację matematyczną, niezależnie od tego, czy zdajesz sobie sprawę, czy nie, to jednak przyjmu- jemy niezwykle ważne założenie dotyczące tej wartości: to jest konkretna wartość teraz. var x, y = 2; console.log( x + y ); // Wynik: NaN -- poniewa(cid:298) warto(cid:286)(cid:252) x nie zosta(cid:225)a jeszcze przypisana. W przypadku operacji x + y przyjęte zostaje założenie, że obie wartości (x i y) zostały zdefiniowane. Pod względami, które wyjaśnię za chwilę, przyjmujemy założenie, że wartości x i y zostały rozwiązane. Byłoby nonsensem oczekiwać, że operator + sam z siebie jest w stanie wstrzymywać się z prze- prowadzeniem operacji aż do chwili, gdy obie wartości (x i y) zostaną rozwiązane (będą gotowe). Takie podejście doprowadziłoby do powstania chaosu w programie, ponieważ wykonanie niektó- rych poleceń kończyłoby się teraz, natomiast innych później. Jak można traktować relacje między dwoma poleceniami, gdy którekolwiek z nich (lub nawet oba) nie zostały jeszcze zakończone? Jeżeli działanie polecenia nr 2 opiera się na zakończeniu wy- konywania polecenia nr 1, to mamy dwie możliwe sytuacje. Pierwsza — polecenie nr 1 będzie za- kończone teraz i wszystko przebiegnie zgodnie z oczekiwaniami. Druga — wykonywanie polece- nia nr 1 jeszcze się nie zakończyło, a więc działanie polecenia nr 2 zakończy się niepowodzeniem. Jeżeli taka sytuacja brzmi dla Ciebie znajomo (patrz rozdział 1.), to dobrze! Powracamy do omawianej operacji matematycznej x + y. Wyobraź sobie, że chcesz powiedzieć: „Dodaj x i y, ale jeśli którakolwiek z wymienionych wartości nie jest jeszcze gotowa, to poczekaj i dodaj je natychmiast, gdy tylko stanie się to możliwe”. Być może w tej chwili pomyślałeś o wywołaniach zwrotnych. Dobrze… function add(getX,getY,cb) { var x, y; getX( function(xVal){ x = xVal; // Czy obie warto(cid:286)ci s(cid:261) gotowe? if (y != undefined) { Czym jest obietnica? (cid:95) 53 Poleć książkęKup książkę cb( x + y ); // Obliczenie sumy. } } ); getY( function(yVal){ y = yVal; // Czy obie warto(cid:286)ci s(cid:261) gotowe? if (x != undefined) { cb( x + y ); // Obliczenie sumy. } } ); } // Funkcje fetchX() i fetchY() s(cid:261) // synchroniczne lub asynchroniczne. add( fetchX, fetchY, function(sum){ console.log( sum ); // To by(cid:225)o (cid:225)atwe, prawda? } ); Poświęć chwilę na podziwianie piękna (lub jego brak) powyższego fragmentu kodu. Wprawdzie brzydota powyższego kodu pozostaje niezaprzeczalna, ale jednocześnie warto zwrócić uwagę na ważny aspekt przedstawionego podejścia asynchronicznego. W omawianym fragmencie kodu x i y potraktowaliśmy jako przyszłe wartości, a zdefiniowana ope- racja add(..) nie sprawdza, czy obie wymienione wartości są od razu dostępne. Innymi słowy znor- malizowaliśmy teraz i później, co pozwala opierać się na przewidywalnym wyniku operacji add(..). Dzięki użyciu funkcji add(..) przyjęte podejście pozostaje chwilowo spójne — mamy takie samo zachowanie dla teraz i później — znacznie łatwiej zrozumieć działanie przedstawionego kodu asynchronicznego. Ujmując rzecz jeszcze prościej — nieustannie obsługujemy zarówno teraz, jak i później. Obie ope- racje stają się typu później i wszystkie są asynchroniczne. Oczywiście tego rodzaju podejście oparte na wywołaniach zwrotnych pozostawia wiele do życzenia. To jest pierwszy mały krok na drodze do poznania korzyści wynikających z przyszłych wartości bez przejmowania się aspektem czasu — kiedy i czy w ogóle wspomniane wartości będą dostępne. Wartość obietnicy Więcej informacji szczegółowych o obietnicach na pewno przedstawię w dalszej części rozdziału, więc nie przejmuj się, jeśli coś jest jeszcze dla Ciebie niezrozumiałe. Poniżej pokazałem, jak przy- kład dodawania liczb x i y można wyrazić za pomocą funkcji obietnic. function add(xPromise,yPromise) { // Wywo(cid:225)anie Promise.all([ .. ]) pobiera tablic(cid:266) obietnic. // Warto(cid:286)ci(cid:261) zwrotn(cid:261) jest nowa obietnica oczekuj(cid:261)ca // na zako(cid:276)czenie wszystkich pozosta(cid:225)ych. return Promise.all( [xPromise, yPromise] ) // Kiedy obietnica zostanie rozwi(cid:261)zana, pobieramy // otrzymane warto(cid:286)ci X i Y , a nast(cid:266)pnie je dodajemy. .then( function(values){ // W poni(cid:298)szym poleceniu values to tablica komunikatów // otrzymanych z wcze(cid:286)niej rozwi(cid:261)zanych obietnic. return values[0] + values[1]; } ); } 54 (cid:95) Rozdział 3. Obietnice Poleć książkęKup książkę // Funkcje fetchX() i fetchY() zwracaj(cid:261) obietnice dla // odpowiednich warto(cid:286)ci, które mog(cid:261) by(cid:252) dost(cid:266)pne // teraz lub pó(cid:296)niej. add( fetchX(), fetchY() ) // Mamy obietnic(cid:266) odpowiedzialn(cid:261) za // obliczenie sumy dwóch liczb. // Teraz (cid:225)(cid:261)czymy wywo(cid:225)ania then(..) w oczekiwaniu // na rozwi(cid:261)zanie zwróconej obietnicy. .then( function(sum){ console.log( sum ); // To by(cid:225)o (cid:225)atwiejsze! } ); Mamy dwie warstwy obietnic w powyższym fragmencie kodu. Pierwsza — funkcje fetchX() i fetchY() są wywoływane bezpośrednio, a zwracane przez nie wartości (obietnice!) są przekazywane funkcji add(..). Wprawdzie wartości wspomnianych obietnic mogą być dostępne teraz lub później, ale zachowanie każdej obietnicy jest znormalizowane, aby było ta- kie samo niezależnie od okoliczności. Wartości X i Y używamy w sposób niezależny od czasu. To są przyszłe wartości. Druga warstwa to obietnica, że funkcja add(..) utworzy (za pomocą wywołania Promise.all([..])) i zwróci odpowiednie dane, na które oczekujemy za pomocą wywołania then(..). Kiedy zakończy się wykonywanie operacji add(..), przyszła wartość sum jest już dostępna i można ją wyświetlić. Wewnątrz funkcji add(..) ukrywamy logikę oczekiwania na przyszłe wartości X i Y. Wewnątrz funkcji add(..) wywołanie Promise.all([ .. ]) tworzy obietnicę (oczekującą na rozwiązanie promiseX i promiseY). Wywołanie połączone z .then(..) tworzy inną obietnicę, której wiersz values[0] + values[1] dostarcza natychmiastowe rozwiązanie (wynik opera- cji dodawania). Dlatego też wywołanie then(..) znajdujące się na końcu wywołania add(..) — na końcu fragmentu kodu — jest w rzeczywistości wykonywane względem drugiej zwróco- nej obietnicy, a nie pierwszej utworzonej przez Promise.all([ .. ]). Ponadto do drugiego wywołania then(..) nie dołączamy kolejnego, następuje utworzenie kolejnej obietnicy, którą będziemy obserwować i której będziemy używać. Szczegółowe omówienie tego rodzaju łańcu- cha obietnic przedstawię w dalszej części rozdziału. Podobnie jak w przypadku zamówienia cheeseburgera, istnieje niebezpieczeństwo, że rozwiązanie obietnicy zakończy się niepowodzeniem (odrzuceniem), a nie sukcesem (spełnieniem). W prze- ciwieństwie do spełnionej obietnicy, gdzie wartość zawsze jest programowa, wartość w sytuacji nie- powodzenia — zwykle określana mianem powodem odrzucenia — może być przypisana bezpo- średnio przez logikę programu lub też być przypisana pośrednio na skutek zgłoszenia wyjątku w trakcie działania aplikacji. W obietnicy wywołanie then(..) może w rzeczywistości pobrać dwie funkcje, pierwszą, przeznaczoną do obsługi sytuacji spełnienia (jak pokazałem wcześniej), i drugą, do obsługi sytuacji odrzucenia: add( fetchX(), fetchY() ) .then( // Procedura obs(cid:225)ugi w sytuacji spe(cid:225)nienia obietnicy. function(sum) { console.log( sum ); }, Czym jest obietnica? (cid:95) 55 Poleć książkęKup książkę // Procedura obs(cid:225)ugi w sytuacji odrzucenia obietnicy. function(err) { console.error( err ); // Koszmar! } ); Jeżeli wystąpi niepowodzenie podczas pobierania wartości X lub Y bądź też niepowodzenie w trak- cie operacji dodawania, to obietnica zwracana przez add(..) zostanie odrzucona. Wartość z obietnicy będzie przekazana drugiej procedurze obsługi zdefiniowanej w then(..). Ponieważ obietnica hermetyzuje stan zależny od czasu — oczekiwanie na spełnienie lub odrzucenie — więc z zewnątrz sama w sobie pozostaje niezależna od czasu, a tym samym może być złożona (łą- czona) w przewidywalny sposób niezależnie od czasu lub otrzymanego wyniku. Co więcej, po rozwiązaniu obietnicy pozostaje taka na zawsze — staje się wartością niemodyfiko- walną w danym miejscu — i może być pobierana wielokrotnie, w zależności od potrzeb. Ponieważ po rozwiązaniu obietnica pozostaje niemodyfikowana na zewnątrz, wartość tę moż- na bezpiecznie przekazywać do na przykład firm trzecich bez obaw o jej przypadkową lub zło- śliwą modyfikację. W szczególności dotyczy to sytuacji, gdy wiele komponentów jest zaintere- sowanych rozwiązaniem obietnicy. Dany komponent nie ma żadnego wpływu na możliwość prowadzonej przez inny obserwacji rozwiązania obietnicy. Wprawdzie niemodyfikowalność może wydawać się zagadnieniem akademickim, ale tak naprawdę to jeden z podstawowych i najważniejszych aspektów projektu obietnicy, którego nie można zignorować. Przedstawiłem jedną z oferujących największe możliwości i jedną z najważniejszych koncepcji doty- czących obietnic. Jeżeli poświęcisz dużo pracy, to ten sam efekt możesz osiągnąć za pomocą zawiłego połączenia wywołań zwrotnych. To jednak naprawdę nie będzie efektywna strategia, zwłaszcza jeśli będziesz zmuszony nieustannie ją stosować. Obietnica dostarcza łatwego do wielokrotnego użycia mechanizmu hermetyzacji i tworzenia przyszłych wartości. Zdarzenie ukończenia Jak mogłeś zobaczyć, pojedyncza obietnica zachowuje się, jak przyszła wartość. Istnieje jeszcze inny sposób traktowania rozwiązania obietnicy: mechanizm kontroli przepływu działania programu („tym- czasowe to, a później to”) dla co najmniej dwóch kroków w zadaniu asynchronicznym. Przyjmujemy założenie wywołania funkcji foo(..) w celu wykonania pewnego zadania. Nie znamy lub też nie przejmujemy się wieloma szczegółami dotyczącymi tego zadania. Jego wykonanie może zakończyć się od razu lub zająć pewną ilość czasu. Musimy jedynie wiedzieć, kiedy funkcja foo(..) zakończy działanie, co pozwoli nam na przejście do kolejnego zadania. Innymi słowy, potrzebny jest mechanizm poinformowania o zakończeniu wy- konywania funkcji foo(..), który pozwoli na kontynuację działania programu. W sposób typowy dla JavaScript sprawa przedstawia się następująco — jeżeli zachodzi potrzeba nasłuchiwania powiadomień, to prawdopodobnie przychodzą Ci na myśl zdarzenia. Dlatego na- szą potrzebę można wyrazić jako potrzebę nasłuchiwania zdarzenia ukończenia (lub inaczej zdarzenia kontynuacji) emitowanego przez foo(..). 56 (cid:95) Rozdział 3. Obietnice Poleć książkęKup książkę Nazwa wspomnianego zdarzenia (ukończenia lub kontynuacji) zależy od Twojej perspektywy. Czy koncentrujesz się bardziej na operacjach wykonywanych za pomocą funkcji foo(..), czy jednak na tym, co będzie po zakończeniu działania wymienionej funkcji? Obie perspektywy są poprawne i użyteczne. Zdarzenie powiadomienia informuje nas, że wykonywanie funkcji foo(..) zostało zakończone i można przejść do kolejnego kroku. Istotnie, wywołanie zwrotne przekazane do wywołania przez zdarzenia powiadomienia samo w sobie jest tym, co wcześniej określiliśmy mianem kontynuacji. Zdarzenie ukończenia jest nieco bardziej skoncentrowane na funkcji foo(..), która aktualnie nas bardzo interesuje. Dlatego też w pozostałej części roz- działu będę używał nazwy zdarzenie ukończenia. W podejściu opartym na wywołaniach zwrotnych powiadomieniem będzie wywołanie zwrotne uruchomione przez zadanie (funkcja foo(..)). Z kolei w przypadku obietnic następuje odwrócenie sytuacji i to funkcja foo(..) nasłuchuje zdarzenia, po którego otrzymaniu odpowiednio kontynuuje wykonywanie kodu. Na początek spójrz na przedstawiony poniżej pseudokod: foo(x) { // Rozpocz(cid:266)cie zadania, którego wykonanie mo(cid:298)e chwil(cid:266) zabra(cid:252). } foo( 42 ) on (foo completion ) { // Teraz mo(cid:298)na przej(cid:286)(cid:252) do kolejnego kroku! } on (foo error ) { // Ups! Co(cid:286) posz(cid:225)o nie tak w funkcji foo(..). } Wywołujemy funkcję foo(..), a następnie definiujemy dwie procedury nasłuchiwania zdarzeń, po jednej dla zdarzeń completion i error. Te zdarzenia przedstawiają dwa możliwe sposoby zakoń- czenia działania funkcji foo(..). Ogólnie rzecz ujmując, funkcja foo(..) nawet nie „wie”, że wy- wołujący ją kod nasłuchuje wymienionych zdarzeń, co stanowi doskonały przykład separacji zadań. Niestety, przedstawione podejście wymaga zastosowania pewnej magii środowiska JavaScript, która nie istnieje (i prawdopodobnie byłaby nieco niepraktyczna). Poniżej przedstawiłem znacznie bardziej naturalny sposób wyrażenia w kodzie JavaScript podejścia opartego na obietnicach: function foo(x) { // Rozpocz(cid:266)cie zadania, którego wykonanie mo(cid:298)e chwil(cid:266) potrwa(cid:252). // Komponent nas(cid:225)uchuj(cid:261)cy zdarze(cid:276) powinien // mie(cid:252) mo(cid:298)liwo(cid:286)(cid:252) odpowiedniego zareagowania. return listener; } var evt = foo( 42 ); evt.on( completion , function(){ // Teraz mo(cid:298)na przej(cid:286)(cid:252) do kolejnego kroku! } ); evt.on( failure , function(err){ // Ups! Co(cid:286) posz(cid:225)o nie tak w funkcji foo(..). } ); Czym jest obietnica? (cid:95) 57 Poleć książkęKup książkę Funkcja foo(..) zapewnia wyraźną możliwość dostarczania danych z mechanizmu subskrypcji zda- rzeń, a kod wywołujący otrzymuje i rejestruje dwie procedury obsługi zdarzeń. Odwrotne działanie w porównaniu z tradycyjnym kodem bazującym na wywołaniach zwrotnych powinno być oczywiste i jest pożądane. Zamiast przekazywać wywołanie zwrotne funkcji foo(..), mamy obiekt evt otrzymujący wywołania zwrotne. Jak sobie przypominasz z rozdziału 2., wywołania zwrotne same w sobie przedstawiają odwrócenie kontroli. Dlatego też odwrócenie wzorca wywołań zwrotnych można w rzeczywistości uznać za „odwrócenie odwrócenia”, czyli inaczej brak odwrócenia kontroli — przywrócenie kontroli z powro- tem do pierwotnego kodu. Mamy więc rozwiązanie, którego oczekiwaliśmy od samego początku. Jedną z ważniejszych korzyści jest to, że wiele oddzielnych fragmentów kodu otrzymuje możliwość nasłuchiwania zdarzeń. Wszystkie nasłuchujące komponenty będą niezależnie od siebie poinfor- mowane o zakończeniu działania funkcji foo(..), co pozwoli im na wykonanie kolejnych kroków: var evt = foo( 42 ); // Funkcja bar(..) nas(cid:225)uchuje zdarzenia rozg(cid:225)aszanego przez foo(..). bar( evt ); // Funkcja baz(..) równie(cid:298) nas(cid:225)uchuje zdarzenia rozg(cid:225)aszanego przez foo(..). baz( evt ); Brak odwrócenia kontroli pozwala na zastosowanie eleganckiego podziału zadań, a funkcje bar(..) i baz(..) nie muszą być angażowane w sposób wywołania foo(..). Podobnie funkcja foo(..) nie musi nic wiedzieć o istnieniu bar(..) i baz(..) lub o oczekiwaniu przez inny komponent na powiado- mienie o zakończeniu działania foo(..). W zasadzie obiekt evt jest neutralnym łącznikiem zewnętrznym między oddzielnymi zadaniami. „Zdarzenia” obietnicy Jak pewnie się domyśliłeś, możliwość nasłuchiwania zdarzeń przez obiekt evt stanowi analogią dla obietnicy. W przypadku podejścia opartego na obietnicy przedstawiony wcześniej fragment kodu utworzy foo(..) i zwróci egzemplarz obietnicy Promise, która następnie zostanie przekazana do bar(..) i baz(..). Nasłuchiwane „zdarzenia” rozwiązania obietnicy tak naprawdę nie są zdarzeniami (choć za- chowują się właśnie jak zdarzenia) i zwykle nie są nazywane completion lub error. Zamiast tego używamy then(..) do zarejestrowania zdarzenia then. Ujmując rzecz precyzyjniej, then(..) rejestruje zdarzenia fulfillment i (lub) rejection, mimo że wymienione słowa nie są wy- raźnie stosowane w kodzie. Spójrz na przedstawiony poniżej fragment kodu: function foo(x) { // Rozpocz(cid:266)cie zadania, którego wykonanie mo(cid:298)e chwil(cid:266) potrwa(cid:252). // Przygotowanie i zwrot obietnicy. return new Promise( function(resolve,reject){ 58 (cid:95) Rozdział 3. Obietnice Poleć książkęKup książkę // Ostatecznie nast(cid:266)puje wywo(cid:225)anie resolve(..) lub reject(..), // które to funkcje s(cid:261) wywo(cid:225)aniami zwrotnymi // rozwi(cid:261)zania obietnicy. } ); } var p = foo( 42 ); bar( p ); baz( p ); Powyższy wzorzec wraz z wywołaniem new Promise( function(..){ .. } ) jest określany mianem konstruktora z ujawnieniem (ang. revealing constructor). Przekazywana funkcja jest wykonywana natychmiast (nie ma asynchronicznego odroczenia jej uruchomienia, jak ma to miejsce w przypadku wywołań zwrotnych do then(..)) i otrzymuje dwa parametry. W omawia- nym przypadku to resolve i reject. To są funkcje rozwiązania obietnicy. Funkcja resolve(..) wskazuje na spełnienie obietnicy, natomiast reject(..) na jej odrzucenie. Prawdopodobnie domyśliłeś się, jak może przedstawiać się kod wewnątrz funkcji bar(..) i baz(..): function bar(fooPromise) { // Nas(cid:225)uchiwanie uko(cid:276)czenia foo(..). fooPromise.then( function(){ // Dzia(cid:225)anie funkcji foo(..) zosta(cid:225)o zako(cid:276)czone, // wi(cid:266)c funkcja bar(..) mo(cid:298)e wykona(cid:252) swoje zadanie. }, function(){ // Ups! Co(cid:286) posz(cid:225)o nie tak w funkcji foo(..). } ); } // To samo dotyczy funkcji baz(..). Rozwiązanie obietnicy niekoniecznie wymaga wysłania komunikatu, jak miało to miejsce podczas analizy obietnic jako przyszłych wartości. Może to być po prostu sygnał kontroli przepływu działania programu; takie właśnie podejście zastosowałem we wcześniejszym fragmencie kodu. Inne rozwiązanie przedstawia się następująco: function bar() { // Dzia(cid:225)anie funkcji foo(..) zosta(cid:225)o zako(cid:276)czone, // wi(cid:266)c funkcja bar(..) mo(cid:298)e wykona(cid:252) swoje zadanie. } function oopsBar() { // Ups! Poniewa(cid:298) co(cid:286) posz(cid:225)o nie tak w funkcji foo(..), // wi(cid:266)c nie dosz(cid:225)o do uruchomienia funkcji bar(..). } // To samo dotyczy funkcji baz() i oopsBaz(). var p = foo( 42 ); p.then( bar, oopsBar ); p.then( baz, oopsBaz ); Czym jest obietnica? (cid:95) 59 Poleć książkęKup książkę Jeżeli już wcześniej spotkałeś się z kodem opartym na obietnicach, to być może sądzisz, że dwa ostatnie wiersze powyższego kodu można zapisać w postaci p.then( .. ).then( .. ), czyli z zastosowaniem łączenia zamiast po prostu jako p.then(..); p.then(..). Jednak taka zmiana spowodowałaby zdefiniowanie zupełnie innego zachowania, więc bądź szczególnie ostrożny! Różnica może nie być od razu widoczna, ale tak naprawdę mamy tutaj do czynienia z całkiem innym wzorcem asynchronicznym, z którym się dotąd nie spotkaliśmy: splitting (podział) kontra forking (rozwidlenie). Nie przejmuj się! Do tego zagadnienia jeszcze powró- cimy w rozdziale. Zamiast przekazać obietnicę p do funkcji bar(..) i baz(..), wykorzystujemy obietnicę do kontrolowa- nia, kiedy wymienione funkcje mają zostać wykonane, o ile w ogóle. Podstawowa różnica sprowadza się do procedury obsługi błędów. W podejściu przedstawionym w pierwszym fragmencie kodu funkcja bar(..) jest wywoływana nie- zależnie od wyniku działania funkcji foo(..) — sukces bądź niepowodzenie. Tutaj funkcja bar(..) ma własną logikę do wykonania, gdy zostanie poinformowana o zakończonym niepowodzeniem uruchomieniu foo(..). Oczywiście to samo dotyczy również funkcji baz(..). W drugim fragmencie kodu funkcja bar(..) jest wywoływana tylko wtedy, gdy działanie foo(..) zakończy się powodzeniem. W przeciwnym razie nastąpi wywołanie oopsBar(..). To samo dotyczy także funkcji baz(..). Tak naprawdę żadnego z wymienionych powyżej podejść nie można uznać za jedyne poprawne. Zdarzają się różne sytuacje, w których preferowane będzie raz jedno, a raz drugie. Niezależnie od sytuacji obietnica p pochodząca z funkcji foo(..) zostanie użyta do określenia ko- lejnych podejmowanych działań. Co więcej, ponieważ oba fragmenty kodu kończą się dwukrotnym wywołaniem then(..) wzglę- dem tej samej obietnicy p, otrzymujemy potwierdzenie dla wcześniejszego stwierdzenia, że obietnica (po rozwiązaniu) na zawsze zachowuje konkretne rozwiązanie (spełnienie lub odrzucenie) i tym samym może być obserwowana dowolną liczbę razy. Po rozwiązaniu obietnicy p kolejny krok zawsze będzie taki sam, zarówno dla teraz, jak i później. Określanie typu na podstawie then() W świecie obietnic mamy jeden niezwykle ważny szczegół. Skąd można wiedzieć, czy pewna war- tość jest autentyczną obietnicą, czy nią nie jest? Ujmując rzecz jeszcze bardziej bezpośrednio: czy dana wartość będzie zachowywała się jak obietnica? Biorąc pod uwagę fakt konstruowania obietnic za pomocą składni new Promise(..), możesz uznać, że polecenie p instanceof Promise jest wystarczającym sprawdzeniem. Niestety, istnieje wiele powo- dów, dla których tak nie jest. Przede wszystkim wartość obietnicy możesz otrzymać z innego okna przeglądarki internetowej (element iframe itd.), które będzie miało własną obietnicę inną niż znajdująca się w bieżącym oknie lub ramce. W takim przypadku identyfikacja egzemplarza obietnicy zakończy się niepowodzeniem. 60 (cid:95) Rozdział 3. Obietnice Poleć książkęKup książkę Co więcej, biblioteka lub framework mogą stosować własne obietnice zamiast oferowanej przez specyfikację ES6 natywnej implementacji (Promise). Tak naprawdę w starszych wersjach przeglą- darek internetowych pozbawionych implementacji Promise użycie bibliotek przeznaczonych do obsługi obietnic może sprawdzać się doskonale. Kiedy w dalszej części rozdziału będziemy analizować proces rozwiązywania obietnicy, oczywiste stanie się, dlaczego ogromne znaczenie ma możliwość sprawdzania i prawidłowego rozpoznawania wartości niebędących autentycznymi obietnicami. Teraz musisz uwierzyć mi na słowo, że to ma znaczenie krytyczne dla funkcjonowania całości. Dlatego też sposobem pozwalającym na rozpoznanie obietnicy (lub komponentu o działaniu przy- pominającym obietnicę) byłoby zdefiniowanie tak zwanego thenable, czyli obiektu zawierającego metodę o nazwie then(..). Przyjmuje się założenie, że wszelka tego rodzaju wartość jest zgodna z obietnicą. Ogólne pojęcie dla sprawdzania typu po przyjęciu założenia o wartości typu w oparciu o istnienie pewnych właściwości to tak zwane kacze typowanie (ang. duck typing). „Jeżeli coś wygląda jak kaczka i wydaje dźwięki takie jak kaczka to musi być kaczką” — patrz inna książka z tej serii, zaty- tułowana Typy i składnia. Poniżej przedstawiłem przykład kaczego typowania pozwalającego na sprawdzenie, czy obiekt można uznać za thenable, czyli zawierający metodę then(..): if ( p !== null ( typeof p === object || typeof p === function ) typeof p.then === function ) { // Przyjmujemy za(cid:225)o(cid:298)enie, (cid:298)e obiekt jest thenable! } else { // Obiekt nie zawiera metody then(..). } Fuj! Pomijając fakt, że przedstawiona powyżej brzydka logika musi być zaimplementowana w różnych miejscach, zdefiniowany tutaj kod zawiera jeszcze o wiele poważniejsze błędy. Jeżeli spróbujesz spełnić obietnicę za pomocą dowolnej wartości obiektu (lub funkcji) zawierającego wewnątrz funkcję then(..), ale nie chcesz tego obiektu traktować jako obietnicy lub thenable, to powstanie problem. Wspomniany obiekt będzie uznany za thenable, a tym samym potraktowany według specjalnych reguł (przedstawię je w dalszej części rozdziału). Z wymienioną sytuacją mamy do czynienia nawet wtedy, kiedy nie wiemy, że dany element zawiera funkcję then(..). Spójrz na poniższy fragment kodu: var o = { then: function(){} }; // Poni(cid:298)sze polecenie powoduje, (cid:298)e v jest po(cid:225)(cid:261)czone z o w stylu [[Prototype]] . var v = Object.create( o ); v.someStuff = (cid:258)wietnie ; v.otherStuff = nie tak (cid:258)wietnie ; v.hasOwnProperty( then ); // Fa(cid:225)sz. Określanie typu na podstawie then() (cid:95) 61 Poleć książkęKup książkę Element v w ogóle nie wygląda jak obietnica. To jest po prostu zwykły obiekt wraz z pewnymi właściwościami. Prawdopodobnie planujesz przekazywać wartość v tak jak każdy inny obiekt. Jednak nie zdajesz sobie sprawy, że obiekt v jest również połączony ([[Prototype]] — patrz inna książka z tej serii, zatytułowana Wskaźnik this i prototypy obiektów) z innym obiektem o, który ma zdefiniowaną metodę then(..). Dlatego też w trakcie operacji kaczego typowania następuje przyjęcie założenia, że obiekt v jest thenable. O nie! Wspomniane połączenie nawet nie musi być utworzone celowo, jak przedstawiłem poniżej: Object.prototype.then = function(){}; Array.prototype.then = function(){}; var v1 = { hello: (cid:258)wiecie }; var v2 = [ Hello , (cid:165)wiecie ]; W powyższym fragmencie kodu obiekty v1 i v2 są thenable. Nie możesz kontrolować lub przewi- dzieć, czy jakikolwiek inny kod przypadkowo bądź złośliwie nie wstawi metody then(..) do Object. prototype, Array.prototype lub innego natywnego prototypu. Ponadto jeśli wskazany element jest funkcją niewywołującą żadnych parametrów jako wywołań zwrotnych, to każda obietnica rozwią- zania za pomocą tego rodzaju wartości będzie w sposób niezauważony zawieszona na zawsze! Ist- ne szaleństwo. To brzmi mało prawdopodobnie lub wręcz nieprawdopodobnie? Być może. Musisz jednak pamiętać, że jeszcze przed wydaniem ES6 istniało wiele doskonale znanych bibliotek niebazujących na obietnicach, ale zawierających metody o nazwie then(..). W przypadku części ze wspomnianych bibliotek ich twórcy zdecydowali się na zmianę nazw metod, aby tym samym uniknąć kolizji nazw (to jest rozwiązanie do niczego!). Natomiast w przypadku innych autorzy po prostu ograniczyli się do komunikatu informującego o niezgodności biblioteki z kodem opartym na obietnicach, ponieważ nie potrafili sobie poradzić inaczej. Podjęta przez twórców standardu ES6 decyzja o przyjęciu nazwy wcześniej niezarezerwowanej — i dotyczącej całkowicie ogólnej koncepcji — prawdopodobnie prowadzi do powstawania błędów, któ- rych wykrycie jest trudne. Przecież nazwa właściwości then oznacza, że żadna wartość (lub jakikolwiek jej delegat) w przeszłości, teraźniejszości i przyszłości nie może mieć funkcji o nazwie then(..), za- równo nazwanej tak przypadkowo, jak i celowo. W przeciwnym razie wspomniana wartość bę- dzie w systemach bazujących na obietnicach uznana za thenable. Nie podoba mi się przyjęte rozwiązanie polegające na użyciu kaczego typowania do rozpozna- wania obietnic. Można było zastosować inne rozwiązania, na przykład „branding” lub nawet „antybranding”, które wydają się być kompromisowe. Jednak to nie jest jeszcze powód do smutku i zniechęcenia. Jak się przekonasz w dalszej części tekstu, kacze typowanie może oka- zać się użyteczne. Nie zapominaj tylko, że jednocześnie będzie niebezpieczne, jeśli element niebędący obietnicą zostanie błędnie za nią uznany. 62 (cid:95) Rozdział 3. Obietnice Poleć książkęKup książkę Kwestie zaufania i obietnice Wcześniej przedstawiłem dwie solidne analogie wyjaśniające różne aspekty użycia obietnic w ko- dzie działającym asynchronicznie. Jednak jeśli na tym poprzestaniemy, to pominiemy prawdopo- dobnie najważniejszą cechę charakterystyczną wzorca bazującego na obietnicach, czyli zaufanie. Podczas gdy analogie przyszłej wartości i zdarzenia ukończenia odgrywały wyraźną rolę we wzor- cach kodu przeanalizowanych w poprzednim rozdziale, to nie całkiem pozostaje jasne, dlaczego lub jak obietnice zostały zaprojektowane do rozwiązania przedstawionych w rozdziale 2. problemów związanych z odwróceniem kontroli. Jeżeli zaczniesz dokładniej analizować koncepcję, to odkry- jesz pewne istotne możliwości pozwalające na przywrócenie zaufania w kodzie działającym asyn- chronicznie. Zaczynamy od przypomnienia problemów z zaufaniem, które występują w kodzie bazującym jedynie na wywołaniach zwrotnych. Po przekazaniu wywołania zwrotnego do narzędzia foo(..) możemy się spodziewać: (cid:120) zbyt wczesnego uruchomienia wywołania zwrotnego; (cid:120) zbyt późnego (lub nawet braku) uruchomienia wywołania zwrotnego; (cid:120) uruchomienia wywołania zwrotnego zbyt małą lub zbyt dużą liczbę razy; (cid:120) niepowodzenia podczas przekazywania wszelkich niezbędnych zmiennych środowiskowych lub parametrów; (cid:120) ukrycia wszelkich błędów lub wyjątków, które mogą zostać zgłoszone. Obietnica ma cechy charakterystyczne, które celowo zostały jej nadane, aby zapewnić użyteczne, powtarzalne rozwiązania wszystkich wymienionych powyżej problemów. Zbyt wczesne wywołanie W przypadku zbyt wczesnego wywołania w kodzie może powstać efekt Zalgo (patrz rozdział 2.), gdy raz zadanie jest kończone synchronicznie, a innym razem asynchronicznie, co może prowadzić do wystąpienia stanu wyścigu. Obietnica z definicji nie jest podatna na omawiany problem, ponieważ zdarzenie natychmiast spełnionej obietnicy (na przykład new Promise(function(resolve){ resolve(42); })) nie może być synchroniczne. Dlatego też po wywołaniu metody then(..) w obietnicy, nawet jeśli została rozwiązana, to wywo- łania zwrotne dostarczane do then(..) zawsze będą uruchamiane asynchronicznie (więcej infor- macji na ten temat przedstawiłem w rozdziale 1., w podrozdziale „Zadania”). Nie trzeba dłużej stosować sztuczek typu wywołania setTimeout(..,0). Obietnica automatycznie uniemożliwia powstanie efektu Zalgo. Kwestie zaufania i obietnice (cid:95) 63 Poleć książkęKup książkę Zbyt późne wywołanie Podobnie jak w poprzednim punkcie, wywołania zwrotne zarejestrowane w metodzie then(..) obietnicy są automatycznie uruchamiane po wywołaniu resolve(..) lub reject(..) przez obietnicę. Wspomniane wywołania zwrotne będą w sposób przewidywalny uruchamiane w kolejnym mo- mencie asynchronicznym (patrz podrozdział „Zadania” w rozdziale 1.). Skoro nie ma możliwości synchronicznego wykonania wywołania zwrotnego, nie istnieje też niebez- pieczeństwo, że synchroniczny łańcuch zadań będzie wykonany w sposób, który w efekcie opóźnia uruchomienie kolejnych wywołań zwrotnych. Oznacza to, że podczas rozwiązywania obietnicy wszystkie zarejestrowane wywołania zwrotne then(..) będą kolejno wykonane, natychmiast w trakcie następnej asynchronicznej chwili (ponownie patrz podrozdział „Zadania” w rozdziale 1.). W po- szczególnych wywołaniach zwrotnych nic nie opóźni ani nie będzie miało wpływu na urucho- mienie kolejnych wywołań zwrotnych. Spójrz na poniższy fragment kodu: p.then( function(){ p.then( function(){ console.log( C ); } ); console.log( A ); } ); p.then( function(){ console.log( B ); } ); // A B C W powyższym kodzie C nie może zakłócić lub poprzedzić wykonania B, co wynika ze sposobu de- finiowania i działania obietnic. Problemy związane z tworzeniem harmonogramu obietnic Trzeba zwrócić uwagę na istnienie wielu niuansów podczas tworzenia harmonogramu obietnic, gdy względna kolejność między wywołaniami zwrotnymi powoduje, że dwie oddzielne obietnice nie będą niezawodnie przewidywalne. Jeżeli dwie obietnice p1 i p2 zostały rozwiązane, to powinno być oczywiste, że polecenia p1.then(..); i p2.then(..); spowodują uruchomienie wywołań zwrotnych w następującej kolejności: najpierw dla p1, później dla p2. Istnieją jednak pewne sytuacje, w których może stać się inaczej, na przykład: var p3 = new Promise( function(resolve,reject){ resolve( B ); } ); var p1 = new Promise( function(resolve,reject){ resolve( p3 ); } ); p2 = new Promise( function(resolve,reject){ resolve( A ); } ); p1.then( function(v){ console.log( v ); } ); 64 (cid:95) Rozdział 3. Obietnice Poleć książkęKup książkę p2.then( function(v){ console.log( v ); } ); // Kolejno(cid:286)(cid:252): A B -- nie B A, jak mo(cid:298)na by(cid:225)o oczekiwa(cid:252). Do tego zagadnienia jeszcze powrócimy w dalszej części rozdziału. Jak możesz zobaczyć, obietnica p1 została rozwiązana nie za pomocą natychmiast dostępnej wartości, ale inną obietnicą p3, której rozwiązaniem jest wartość B. Przedstawione rozwiązanie to rozpakowanie p3 na p1, ale w sposób asynchroniczny. Dlatego też wywołania zwrotne p1 są uruchamiane po wywołaniach zwrotnych p2 w asynchronicznej kolejce zadań (patrz podrozdział „Zadania” w rozdziale 1.). Aby uniknąć tego rodzaju koszmarów, nigdy nie powinieneś polegać na niczym, co dotyczy kolej- ności lub harmonogramu wywołań zwrotnych w różnych obietnicach. Tak naprawdę dobrą praktyką jest uniknięcie tworzenia kodu w sposób, w którym kolejność wielu wywołań zwrotnych w ogóle ma znaczenie. Unikaj takiego podejścia, gdy tylko możesz. Brak uruchomienia wywołania zwrotnego Ta obawa pojawia się niezwykle często. Za pomocą obietnic można ją rozwiać na wiele różnych sposobów. Przede wszystkim nic (nawet błąd JavaScript) nie może uniemożliwić obietnicy wygenerowania powiadomienia o jej rozwiązaniu (o ile oczywiście zostanie rozwiązana). Jeżeli dla obietnicy zareje- strujesz wywołania zwrotne uruchamiane po jej spełnieniu lub odrzuceniu, a obietnica zostanie rozwiązana, to jedno z dwóch wspomnianych wywołań zwrotnych zawsze będzie uruchomione. Oczywiście gdy sam kod wywołań zwrotnych zawiera błędy JavaScript, możesz otrzymać wyniki inne od oczekiwanych, ale to nie zmienia faktu, że wywołania zwrotne zostaną uruchomione. W dal- szej części książki dowiesz się, jak otrzymać powiadomienie o błędzie w wywołaniu zwrotnym, ponie- waż tego rodzaju błędy nie są ukrywane. Co się stanie w sytuacji, gdy obietnica nie zostanie rozwiązana w przedstawiony powyżej sposób? Nawet taka sytuacja została przewidziana przez twórców obietnic, a odpowiedzią jest wyższy poziom abstrakcji o nazwie wyścig: // Funkcja pomocnicza przeznaczona do wyczerpania limitu czasu przeznaczonego dla obietnicy. function timeoutPromise(delay) { return new Promise( function(resolve,reject){ setTimeout( function(){ reject( Czas min(cid:200)(cid:239)! ); }, delay ); } ); } // Konfiguracja wyczerpania limitu czasu dla funkcji foo(). Promise.race( [ foo(), // Próba wywo(cid:225)ania funkcji foo(). timeoutPromise( 3000 ) // Dost(cid:266)pny czas to 3 sekundy. ] ) .then( function(){ // Funkcja foo(..) zosta(cid:225)a wywo(cid:225)ana w podanym czasie! }, Kwestie zaufania i obietnice (cid:95) 65 Poleć książkęKup książkę function(err){ // Funkcja foo() zosta(cid:225)a odrzucona lub po prostu nie by(cid:225)a // uruchomiona w przeznaczonym na to czasie, wi(cid:266)c za pomoc(cid:261) // err podajemy, która z wymienionych sytuacji wyst(cid:261)pi(cid:225)a. } ); W powyższej sytuacji mamy znacznie więcej szczegółów dotyczących przekroczenia limitu czasu w obietnicy, ale do tego zagadnienia jeszcze później wrócimy. Co ważniejsze, możemy być pewni otrzymania sygnału wygenerowanego przez foo() w celu uniknię- cia zawieszenia programu w nieskończoność. Uruchomienie wywołania zwrotnego zbyt małą lub zbyt dużą liczbę razy Z definicji jeden to odpowiednia liczba uruchomień wywołania zwrotnego. Dlatego też „zbyt mało” oznacza tutaj zero wywołań zwrotnych, co właściwie odpowiada przeanalizowanej przed chwilą sytuacji „braku” wywołań zwrotnych. Przypadek „zbyt wiele” jest łatwy do wyjaśnienia. Obietnice są definiowane tak, aby mogły być roz- wiązane tylko jeden raz. Jeżeli z jakiegokolwiek powodu obietnica tworzy kod próbujący wielokrot- nie wywołać funkcje resolve(..) lub reject(..) bądź obie, to obietnica zaakceptuje tylko pierwsze rozwiązanie i po prostu zignoruje pozostałe. Skoro obietnica może być rozwiązana tylko jeden raz, to każde z zarejestrowanych wywołań zwrot- nych then(..) również zostanie uruchomione tylko jednokrotnie. Oczywiście, jeżeli to samo wywołanie zwrotne zarejestrujesz więcej niż tylko jeden raz, na przykład p.then(f); p.then(f);, to liczba uruchomień będzie odpowiadała liczbie rejestracji danego wywoła- nia zwrotnego. Gwarancja jednokrotnego wywołania funkcji odpowiedzi nie chroni Cię przed sytuacją, w której sam strzelasz sobie w stopę. Niepowodzenie podczas przekazywania wszelkich niezbędnych zmiennych środowiskowych lub parametrów Obietnica może mieć co najwyżej jedną wartość rozwiązania (spełnienie lub odrzucenie). Jeżeli obietnica nie zostanie wyraźnie rozwiązana jedną z podanych wyżej wartości, to wartością będzie undefinied, która jest typowa dla kodu utworzonego w JavaScript. Niezależnie od wartości będzie ona zawsze przekazywana do wszystkich zarejestrowanych wywołań zwrotnych (zapewniają- cych obsługę spełnienia i odrzucenia), zarówno teraz, jak i w przyszłości. Musisz koniecznie pamiętać o jednej kwestii. Jeżeli wywołasz resolve(..) lub reject(..) z wielo- ma parametrami, to wszystkie kolejne parametry poza pierwszym zostaną po cichu zignorowane. Wprawdzie może się to wydawać złamaniem wspomnianej wcześniej gwarancji, ale niekoniecznie tak jest, ponieważ większa liczba parametrów oznacza nieprawidłowe użycie mechanizmu obietnicy. Istnieją także mechanizmy ochrony przed innymi nieprawidłowymi sposobami użycia API, na przykład przed wielokrotnym wywołaniem resolve(..). Zachowanie obietnicy można więc uznać za spójne (choć nieco frustrujące). 66 (cid:95) Rozdział 3. Obietnice Poleć książkęKup książkę Jeżeli chcesz przekazać wiele wartości, to musisz je opakować inną pojedynczą wartością, którą następnie przekażesz. Przykładem może być tablica (array) lub obiekt (object). W przypadku środowiska funkcje w JavaScript zawsze zawierają się w zakresie, w którym zostały zdefiniowane (patrz inna książka z tej serii, zatytułowana Zakresy i domknięcia), a więc nadal będą miały dostęp do dostarczonego przez Ciebie elementu. Oczywiście to samo dotyczy również projektu bazującego tylko na wywołaniach zwrotnych, więc to nie jest korzyść specyficzna jedynie dla obietnic, choć jednocześnie jest to gwarancja, na której można polegać. Ukrycie wszelkich błędów lub wyjątków, które mogą zostać zgłoszone W zasadzie jest to powtórzenie przeanalizowanego poprzednio przypadku. Jeżeli obietnica zosta- nie odrzucona wraz z powodem (inaczej z komunikatem o błędzie), to ta właśnie wartość zostanie przekazana do wywołania zwrotnego obsługującego odrzucenie obietnicy. Jednak tutaj mamy kwestię odgrywającą znacznie większą rolę. Jeżeli w dowolnym punkcie two- rzenia obietnicy lub obserwacji jej rozwiązania nastąpi zgłoszenie błędu bądź wyjątku JavaScript, takiego jak TypeError lub ReferenceError, taki wyjątek zostanie przechwycony, a dana obietnica bę- dzie zmuszona do odrzucenia. Spójrz na poniższy fragment kodu: var p = new Promise( function(resolve,reject){ foo.bar(); // Brak definicji foo , a wi(cid:266)c mamy b(cid:225)(cid:261)d! resolve( 42 ); // Nigdy nie dotrzemy do tego wiersza :-( } ); p.then( function fulfilled(){ // Nigdy nie dotrzemy do tego wiersza :-( }, function rejected(err){ // Warto(cid:286)ci(cid:261) err b(cid:266)dzie obiekt wyj(cid:261)tku TypeError, // zg(cid:225)oszonego w wierszu zawieraj(cid:261)cym wywo(cid:225)anie foo.bar(). } ); Wyjątek JavaScript zgłoszony w wierszu zawierającym wywołanie foo.bar() spowoduje odrzucenie obietnicy. Ten wyjątek można przechwycić i obsłużyć. Mamy tutaj do czynienia z niezwykle ważnym szczegółem, ponieważ w efekcie eliminujemy inną potencjalną sytuację, w której może pojawić się Zalgo. Wspomniane błędy mogą spowodować powstanie reakcji synchronicznej, podczas gdy brak błędów będzie asynchroniczny. Obietnice zmieniają zachowanie wyjątków JavaScript na asynchroniczne, a tym samym znacznie zmniejszają niebezpieczeństwo wystąpienia stanu wyścigu. Co się stanie w sytuacji, gdy obietnica będzie spełniona, ale zgłoszenie wyjątku JavaScript nastąpi na etapie obserwacji, to znaczy w zarejestrowanym wywołaniu zwrotnym then(..)? Nawet wówczas błędy nie zostaną utracone, ale sposób ich obsługi może być pewnym zaskoczeniem, dopóki nie zaczniesz analizować dokładniej całej sytuacji: var p = new Promise( function(resolve,reject){ resolve( 42 ); } ); Kwestie zaufania i obietnice (cid:95) 67 Poleć książkęKup książkę p.then( function fulfilled(msg){ foo.bar(); console.log( msg ); // Nigdy nie dotrzemy do tego wiersza :-( }, function rejected(err){ // Nigdy nie dotrzemy równie(cid:298) do tego wiersza :-( } ); Zaczekaj, wydaje się, że w przedstawionym powyżej fragmencie kodu wyjątki zgłaszane przez wywołanie foo.bar() pozostaną ukryte. Nie musisz się obawiać, nie będą ukryte. Mamy jednak nieco poważniejszy błąd, jakim jest brak możliwości nasłuchiwania zgłoszenia wspomnianych wyjątków. Wywołanie zwrotne p.then(..) zwraca inną obietnicę, która z kolei zostanie odrzucona wraz z wy- jątkiem TypeError. Dlaczego nie możemy po prostu wywołać zdefiniowanej tutaj procedury obsługi błędów? Wpraw- dzie wydaje się, że to logiczne rozwiązanie, ale jednocześnie stanowi złamanie podstawowej reguły, jaką jest brak możliwości modyfikacji obietnicy po jej rozwiązaniu. Obietnica p została już spełniona i ma wartość 42. Nie może więc zostać zmieniona na odrzuconą, ponieważ wystąpił błąd podczas obserwacji rozwiązania tej obietnicy. Poza złamaniem jednej z podstawowych reguł obietnicy wspomniane rozwiązanie mogłoby okazać się dewastujące, gdyby dla obietnicy p istniało wiele zarezerwowanych wywołań zwrotnych then(..). W takim przypadku część wywołań byłaby uruchomiona, a inne nie i bardzo trudno byłoby ustalić, dlaczego tak się dzieje. Obietnice, którym można ufać? Ostatnim szczegółem pozostałym do przeanalizowania jest kwestia zaufania w trakcie stosowania wzorca obietnicy. Bez wątpienia zauważyłeś, że stosowanie obietnic nie oznacza całkowitego pozbycia się wywołań zwrotnych. Obietnica po prostu zmienia miejsce, do którego przekazywane jest wywołanie zwrotne. W omawianym przypadku zamiast przekazywać wywołanie zwrotne do foo(..), z wymienionej funkcji otrzymujemy coś (pozornie autentyczną obietnicę) i przekazujemy wywołanie zwrotne w inne miejsce. Mógłbyś w tym miejscu zapytać, dlaczego takie rozwiązanie ma wzbudzać większe zaufanie niż oparte na jedynie wywołaniach zwrotnych. Skąd mamy pewność, że otrzymujemy godną zaufania obietni- cę? Czy to nie jest po prostu domek z kart, a my możemy zaufać tylko dlatego, że nam też się ufa? Jednym z najważniejszych szczegółów dotyczących obietnic i jednocześnie najczęściej przeoczanym jest fakt, że obietnice zawierają także rozwiązanie kwestii zaufania. Znajduje się ono w natywnej implementacji obietnicy ES6, a dokładnie w wywołaniu Promise.resolve(..). Jeżeli funkcji Promise.resolve(..) przekażesz wartość natychmiastową, inną niż obietnica i niebę- dącą thenable, to dana obietnica będzie spełniona za pomocą przekazanej wartości. W przedstawio- nym poniżej fragmencie kodu obietnice p1 i p2 zachowują się w identyczny sposób: var p1 = new Promise( function(resolve,reject){ resolve( 42 ); 68 (cid:95) Rozdział 3. Obietnice Poleć książkęKup książkę } ); var p2 = Promise.resolve( 42 ); Jeżeli funkcji Promise.resolve(..) przekażesz autentyczną obietnicę, to z powrotem otrzymasz tę samą obietnicę: var p1 = Promise.resolve( 42 ); var p2 = Promise.resolve( p1 ); p1 === p2; // Prawda. Co ważniejsze, jeśli funkcji Promise.resolve(..) przekażesz wartość thenable niebędącą obietnicą, to nastąpi próba rozpakowania tej wartości. Proces rozpakowania będzie kontynuowany aż do wy- odrębnienia ostatecznej wartości nieprzypominającej obietnicy. Czy przypominasz sobie to, co wcześniej powiedzieliśmy o wartościach thenable? Spójrz na poniższy fragment kodu: var p = { then: function(cb) { cb( 42 ); } }; // Przedstawione rozwi(cid:261)zanie dzia(cid:225)a, je(cid:286)li tylko masz du(cid:298)o szcz(cid:266)(cid:286)cia. p .then( function fulfilled(val){ console.log( val ); // 42 }, function rejected(err){ // Nigdy nie dotrzemy do tego wiersza. } ); Wprawdzie element p jest thenable, ale to na pewno nie jest autentyczna obietnica. Na szczęście tutaj to uzasadnione rozwiązanie, ponieważ w większości przypadków p będzie obietnicą. Jednak co się stanie, jeśli zastosujemy następujące podejście: var p = { then: function(cb,errcb) { cb( 42 ); errcb( Z(cid:239)owieszczy u(cid:258)miech ); } }; p .then( function fulfilled(val){ console.log( val ); // 42 }, function rejected(err){ // Ups! Ta funkcja nie powinna zosta(cid:252) wywo(cid:225)ana. console.log( err ); // Z(cid:225)owieszczy u(cid:286)miech. } ); Kwestie zaufania i obietnice (cid:95) 69 Poleć książkęKup książkę W powyższym fragmencie kodu element p jest thenable, choć jednocześnie niezbyt dobrze zacho- wuje się jako obietnica. Czy to kod o złośliwym działaniu? Czy to jedynie skutek braku wiedzy pro- gramisty o sposobie działania obietnic? Szczerze mówiąc, to nie ma żadnego znaczenia. Niezależnie od tego, jakie są powody utworzenia tego kodu, nie można mu ufać, jeśli ma taką postać. Dowolną z przedstawionych powyżej wersji p można przekazać funkcji Promise.resolve(..) — wtedy otrzymasz znormalizowany, bezpieczny wynik, którego oczekujesz: Promise.resolve( p ) .then( function fulfilled(val){ console.log( val ); // 42 }, function rejected(err){ // Nigdy nie dotrzemy do tego wiersza. } ); Funkcja Promise.resolve(..) będzie akceptowała dowolną wartość thenable, a następnie rozpakuje ją na jej postać inną niż thenable. Jednak wynikiem wywołania Promise.resolve(..) jest rzeczywista, autentyczna obietnica, której można ufać. Jeżeli przekazana została autentyczna obietnica, to otrzymasz ją z powrotem, a więc nie ma w ogóle żadnych wad zastosowania filtrowania przez funkcję Promise.resolve(..), aby zyskać zaufanie. Przyjmujemy założenie o wywołaniu funkcji pomocniczej foo(..). Nie mamy pewności, czy można zaufać wartości zwrotnej, że będzie zachowywała się jak obietnica. Jednak przynajmniej wiemy, że jest thenable. Dzięki funkcji Promise.resolve(..) otrzymujemy godne zaufania opakowanie obietnicy: // Nie stosuj po prostu poni(cid:298)szego kodu: foo( 42 ) .then( function(v){ console.log( v ); } ); // Zamiast powy(cid:298)szego u(cid:298)yj nast(cid:266)puj(cid:261)cego kodu: Promise.resolve( foo( 42 ) ) .then( function(v){ console.log( v ); } ); Kolejną korzyścią płynącą z opakowania przez Promise.resolve(..) wartości zwrotnej (the- nable lub innej) dowolnej funkcji jest łatwy sposób znormalizowania danego wywołania funk- cji na doskonale zachowujące się zadanie asynchroniczne. Jeżeli wywołanie foo(42) czasami zwraca natychmiastową wartość, a w innych przypadkach obietnicę, to dzięki wywołaniu Pro- mise.resolve( foo(42) ) masz pewność, że wynikiem zawsze będzie obietnica. Uniknięcie powstawania efektu Zalgo skutkuje tworzeniem lepszego kodu. Budowa zaufania Mam nadzieję, że przedstawione wcześniej informacje w pełni wyjaśniły Ci, dlaczego obietnice są warte zaufania, i co ważniejsze, dlaczego wspomniane zaufanie ma znaczenie krytyczne podczas tworzenia solidnego, niezawodnego i łatwego w konserwacji oprogramowania. 70 (cid:95) Rozdział 3. Obietnice Poleć książkęKup książkę Czy w języku JavaScript możesz utworzyć kod asynchroniczny bez zaufania? Oczywiście, że tak. Programiści JavaScript przez niemal dwie dekady tworzyli kod działający asynchronicznie, mając do dyspozycji jedynie wywołania zwrotne. Kiedy zaczniesz kwestionować to, na ile można zaufać wykorzystywanym mechanizmom, czy są w rzeczywistości niezawodne i przewidywalne, to zaczniesz zdawać sobie sprawę, jak niewielkim zaufaniem można obdarzyć wywołania zwrotne. Obietnice to wzór, dzięki któremu wywołania zwrotne otrzymują semantykę godną zaufania. Ich zachowanie staje się bardziej racjonalne i niezawodne. Dzięki uniknięciu odwrócenia kontroli w wywołaniach zwrotnych kontrola pozostaje w systemie godnym zaufania (obietnice), zaprojekto- wanym specjalnie w celu zapewnienia przejrzystości kodu działającego asynchronicznie. Łańcuch przepływu kontroli Wspomniałem już o tym wielokrotnie, ale powtórzę ponownie — obietnica nie stanowi po prostu mechanizmu dla przeprowadzanej w jednym kroku operacji typu „to, a później tamto”. Obietnica na pewno jest elementem konstrukcyjnym, ale okazuje się, że mamy możliwość połączenia wielu obietnic i tym samym przedstawienia sekwencji kroków asynchronicznych. Kluczem, dzięki któremu wspomniane rozwiązanie może działać zgodnie z oczekiwaniami, są dwie cechy nierozłącznie wiążące się z obietnicami: (cid:120) W trakcie każdego wywołania then(..) w obietnicy następuje utworzenie i zwrot nowej obietnicy, z którą można połączyć inną obietnicę. (cid:120) Wartość zwrotna wywołania then(..) spełniająca wywołanie zwrotne (pierwszy parametr) jest automatycznie ustawiona jako wartość spełniająca połączoną obietnicę (z punktu pierwszego). Zacznijmy od zilustrowania powyższej koncepcji, a następnie zobaczymy, jak to pomaga w utworzeniu asynchronicznej sekwencji kontroli przepływu działania programu. Spójrz na poniższy fragment kodu: var p = Promise.resolve( 21 ); var p2 = p.then( function(v){ console.log( v ); // 21 // Spe(cid:225)nienie p2 za pomoc(cid:261) warto(cid:286)ci 42. return v * 2; } ); // Po(cid:225)(cid:261)czenie p2. p2.then( function(v){ console.log( v ); // 42 } ); Przez zwrot obliczonej wartości v * 2 (na przykład 42) spełniamy obietnicę p2, która została utworzona przez pierwsze wywołanie then(..). Po wywołaniu then(..) dla obietnicy p2 jej spełnie- niem będzie wartość wygenerowana przez polecenie return v * 2. Oczywiście p2.then(..) tworzy jeszcze inną obietnicę, którą moglibyśmy przechowywać w zmiennej p3. Łańcuch przepływu kontroli (cid:95) 71 Poleć książkęKup książkę Jednak konieczność utworzenia zmiennej przejściowej p2 (lub p3 itd.) jest nieco irytująca. Na szczę- ście bardzo łatwo możemy połączyć obietnice: var p = Promise.resolve( 21 ); p .then( function(v){ console.log( v ); // 21 // Warto(cid:286)(cid:252) 42 jest spe(cid:225)nieniem po(cid:225)(cid:261)czonej obietnicy. return v * 2; } ) // Tutaj mamy po(cid:225)(cid:261)czon(cid:261) obietnic(cid:266). .then( function(v){ console.log( v ); // 42 } ); W powyższym fragmencie kodu pierwsze wywołanie then(..) to pierwszy kod w sekwencji asynchro- nicznej, natomiast drugie wywołanie then(..) jest drugim krokiem tej sekwencji. Kolejne kroki można dodawać tak długo, dopóki istnieje potrzeba. Połączenie z poprzednim wywołaniem then(..) automatycznie powoduje utworzenie obietnicy. W przedstawionym rozwiązaniu pominęliśmy jeden aspekt. Co się stanie w sytuacji, gdy krok 2. ma zostać wykonany dopiero po zakończeniu asynchronicznego zadania zdefiniowanego w kroku 1.? Używamy polecenia return o natychmiastowym działaniu, co powoduje natychmiastowe spełnienie połączonej obietnicy. Kluczem pozwalającym na zachowanie prawdziwie asynchronicznego działania sekwencji jest sposób funkcjonowania wywołania Promise.resolve(..) po przekazaniu mu obietnicy lub wartości thenable zamiast wartości końcowej. Wywołanie Promise.resolve(..) bezpośrednio zwraca au- tentyczną obietnicę lub rozpakowuje wartość otrzymaną jako thenable — ta operacja jest prze- prowadzana rekurencyjnie aż do rozpakowania wszystkich wartości thenabl
Pobierz darmowy fragment (pdf)

Gdzie kupić całą publikację:

Tajniki języka JavaScript. Asynchroniczność i wydajność
Autor:

Opinie na temat publikacji:


Inne popularne pozycje z tej kategorii:


Czytaj również:


Prowadzisz stronę lub blog? Wstaw link do fragmentu tej książki i współpracuj z Cyfroteką: