Obietnice JavaScript: samouczek z przykładami
Opublikowany: 2022-03-11Obietnice to gorący temat w kręgach programistów JavaScript i zdecydowanie powinieneś się z nimi zapoznać. Nie są łatwe do owinięcia głowy; zrozumienie ich może wymagać kilku samouczków, przykładów i przyzwoitej ilości praktyki.
Moim celem w tym samouczku jest pomóc ci zrozumieć obietnice JavaScript i zachęcić cię do większej praktyki ich używania. Wyjaśnię, czym są obietnice, jakie problemy rozwiązują i jak działają. Każdemu krokowi, opisanemu w tym artykule, towarzyszy przykład kodu jsbin
, który pomoże Ci w pracy i będzie podstawą do dalszej eksploracji.
Co to jest obietnica JavaScript?
Obietnica to metoda, która ostatecznie daje wartość. Można go uznać za asynchroniczny odpowiednik funkcji pobierającej. Jego istotę można wyjaśnić jako:
promise.then(function(value) { // Do something with the 'value' });
Obietnice mogą zastąpić asynchroniczne użycie wywołań zwrotnych i zapewniają kilka korzyści w stosunku do nich. Zaczynają zdobywać popularność, ponieważ coraz więcej bibliotek i frameworków traktuje je jako podstawowy sposób obsługi asynchroniczności. Świetnym przykładem takiego frameworka jest Ember.js.
Istnieje kilka bibliotek, które implementują specyfikację Promises/A+. Nauczymy się podstawowego słownictwa i omówimy kilka przykładów obietnic JavaScript, aby w praktyczny sposób przedstawić stojące za nimi koncepcje. W przykładach kodu użyję jednej z bardziej popularnych bibliotek implementacyjnych, rsvp.js.
Przygotuj się, będziemy rzucać dużo kostkami!
Pobieranie biblioteki rsvp.js
Obietnice, a co za tym idzie rsvp.js, mogą być wykorzystywane zarówno po stronie serwera, jak i klienta. Aby zainstalować go dla nodejs , przejdź do folderu swojego projektu i wpisz:
npm install --save rsvp
Jeśli pracujesz na froncie i używasz altany, to tylko
bower install -S rsvp
z dala.
Jeśli chcesz po prostu dobrze wejść do gry, możesz dołączyć go za pomocą prostego tagu skryptu (a za pomocą jsbin
możesz dodać go za pomocą menu rozwijanego „Dodaj bibliotekę”):
<script src="//cdn.jsdelivr.net/rsvp/3.0.6/rsvp.js"></script>
Jakie właściwości ma obietnica?
Obietnica może znajdować się w jednym z trzech stanów: oczekująca , spełniona lub odrzucona . Po utworzeniu obietnica jest w stanie oczekiwania. Stąd może przejść do stanu spełnionego lub odrzuconego. To przejście nazywamy rozwiązaniem obietnicy . Stan rozwiązany obietnicy jest jej stanem końcowym, więc gdy zostanie spełniona lub odrzucona, pozostaje tam.
Sposobem na stworzenie obietnicy w rsvp.js jest użycie tak zwanego konstruktora ujawniającego. Ten typ konstruktora przyjmuje pojedynczy parametr funkcji i natychmiast wywołuje go z dwoma argumentami, fulfill
i reject
, które mogą zmienić obietnicę w stan fulfilled
lub rejected
:
var promise = new RSVP.Promise(function(fulfill, reject) { (...) });
Ten wzorzec obietnic JavaScript jest nazywany konstruktorem ujawniającym, ponieważ pojedynczy argument funkcji ujawnia swoje możliwości funkcji konstruktora, ale zapewnia, że konsumenci obietnicy nie mogą manipulować jej stanem.
Odbiorcy obietnicy mogą zareagować na zmiany jej stanu, dodając swoją obsługę za pomocą metody then
. Wymaga spełnienia i funkcji obsługi odrzucenia, których obu może brakować.
promise.then(onFulfilled, onRejected);
W zależności od wyniku procesu rozwiązywania obietnicy procedura obsługi onFulfilled
lub onRejected
jest wywoływana asynchronicznie .
Zobaczmy przykład, który pokazuje, w jakiej kolejności rzeczy są wykonywane:
function dieToss() { return Math.floor(Math.random() * 6) + 1; } console.log('1'); var promise = new RSVP.Promise(function(fulfill, reject) { var n = dieToss(); if (n === 6) { fulfill(n); } else { reject(n); } console.log('2'); }); promise.then(function(toss) { console.log('Yay, threw a ' + toss + '.'); }, function(toss) { console.log('Oh, noes, threw a ' + toss + '.'); }); console.log('3');
Ten fragment kodu wyświetla dane wyjściowe podobne do następujących:
1 2 3 Oh, noes, threw a 4.
Lub, jeśli nam się poszczęści, widzimy:
1 2 3 Yay, threw a 6.
Ten samouczek obiecuje pokazuje dwie rzeczy.
Po pierwsze, procedury obsługi, które dołączyliśmy do obietnicy, zostały rzeczywiście wywołane po tym, jak cały inny kod został uruchomiony, asynchronicznie.
Po drugie, że osoba zajmująca się spełnieniem została wezwana tylko wtedy, gdy spełniono obietnicę, z wartością, z jaką została rozwiązana (w naszym przypadku wynik rzutu kostką). To samo dotyczy programu obsługi odrzuceń.
Łączenie obietnic i spływanie w dół
Specyfikacja wymaga, aby funkcja then
(obsługi) również zwracała obietnicę, co umożliwia łączenie obietnic razem, w wyniku czego kod wygląda prawie synchronicznie:
signupPayingUser .then(displayHoorayMessage) .then(queueWelcomeEmail) .then(queueHandwrittenPostcard) .then(redirectToThankYouPage)
Tutaj signupPayingUser
zwraca obietnicę, a każda funkcja w łańcuchu obietnic jest wywoływana z wartością zwracaną przez poprzednią procedurę obsługi po jej zakończeniu. Ze względów praktycznych powoduje to serializację wywołań bez blokowania głównego wątku wykonawczego.
Aby zobaczyć, jak każda obietnica zostanie rozwiązana ze zwróconą wartością poprzedniego elementu w łańcuchu, wracamy do rzucania kośćmi. Chcemy rzucić kostką maksymalnie trzy razy lub do momentu, gdy pojawi się pierwszych sześć jsbin:
function dieToss() { return Math.floor(Math.random() * 6) + 1; } function tossASix() { return new RSVP.Promise(function(fulfill, reject) { var n = Math.floor(Math.random() * 6) + 1; if (n === 6) { fulfill(n); } else { reject(n); } }); } function logAndTossAgain(toss) { console.log("Tossed a " + toss + ", need to try again."); return tossASix(); } function logSuccess(toss) { console.log("Yay, managed to toss a " + toss + "."); } function logFailure(toss) { console.log("Tossed a " + toss + ". Too bad, couldn't roll a six"); } tossASix() .then(null, logAndTossAgain) //Roll first time .then(null, logAndTossAgain) //Roll second time .then(logSuccess, logFailure); //Roll third and last time
Po uruchomieniu tego przykładowego kodu obiecującego zobaczysz na konsoli coś takiego:
Tossed a 2, need to try again. Tossed a 1, need to try again. Tossed a 4. Too bad, couldn't roll a six.
Obietnica zwrócona przez tossASix
jest odrzucana, gdy rzut nie jest szóstką, więc program obsługi odrzuceń jest wywoływany z rzeczywistym rzutem. logAndTossAgain
drukuje wynik na konsoli i zwraca obietnicę, która reprezentuje kolejny rzut kostką. Ten rzut z kolei jest również odrzucany i wylogowywany przez następny logAndTossAgain
.
Czasami jednak masz szczęście* i udaje Ci się wyrzucić szóstkę:
Tossed a 4, need to try again. Yay, managed to toss a 6.
* Nie musisz mieć tyle szczęścia. Istnieje ~42% szansa na wyrzucenie co najmniej jednej szóstki, jeśli rzucisz trzema kośćmi.

Ten przykład uczy nas też czegoś więcej. Widzisz, jak nie było więcej rzutów po pierwszym udanym wyrzuceniu szóstki? Zwróć uwagę, że wszystkie procedury obsługi realizacji (pierwsze argumenty w wywołaniach then
) w łańcuchu są null
, z wyjątkiem ostatniego, logSuccess
. Specyfikacja wymaga, że jeśli handler (spełnienie lub odrzucenie) nie jest funkcją, to zwrócona obietnica musi być rozwiązana (spełniona lub odrzucona) z tą samą wartością. W powyższym przykładzie obietnic funkcja obsługi spełnienia, null
, nie jest funkcją, a wartość obietnicy została spełniona z 6. Tak więc obietnica zwrócona przez wywołanie then
(następne w łańcuchu) również zostanie spełniona z 6 jako jego wartością.
Powtarza się to, dopóki nie pojawi się rzeczywisty moduł obsługi realizacji (taki, który jest funkcją), więc realizacja spływa w dół, dopóki nie zostanie obsłużona. W naszym przypadku dzieje się to na końcu łańcucha, gdzie jest wesoło wylogowany na konsolę.
Obsługa błędów
Specyfikacja Promises/A+ wymaga, aby w przypadku odrzucenia obietnicy lub zgłoszenia błędu w module obsługi odrzucenia, obsługi odrzucenia zajął się „poniżej” źródła.
Wykorzystanie poniższej techniki spływania zapewnia czysty sposób na radzenie sobie z błędami:
signupPayingUser .then(displayHoorayMessage) .then(queueWelcomeEmail) .then(queueHandwrittenPostcard) .then(redirectToThankYouPage) .then(null, displayAndSendErrorReport)
Ponieważ moduł obsługi odrzucenia jest dodawany tylko na samym końcu łańcucha, jeśli jakakolwiek procedura obsługi realizacji w łańcuchu zostanie odrzucona lub zgłosi błąd, spływa ona w dół, aż wpadnie na displayAndSendErrorReport
.
Wróćmy do naszych ukochanych kości i zobaczmy to w akcji. Załóżmy, że chcemy po prostu rzucić kostką asynchronicznie i wydrukować wyniki:
var tossTable = { 1: 'one', 2: 'two', 3: 'three', 4: 'four', 5: 'five', 6: 'six' }; function toss() { return new RSVP.Promise(function(fulfill, reject) { var n = Math.floor(Math.random() * 6) + 1; fulfill(n); }); } function logAndTossAgain(toss) { var tossWord = tossTable[toss]; console.log("Tossed a " + tossWord.toUppercase() + "."); } toss() .then(logAndTossAgain) .then(logAndTossAgain) .then(logAndTossAgain);
Kiedy to uruchomisz, nic się nie dzieje. Na konsoli nic nie jest drukowane i pozornie nie są wyrzucane żadne błędy.
W rzeczywistości błąd jest zgłaszany, po prostu go nie widzimy, ponieważ w łańcuchu nie ma programów obsługi odrzuceń. Ponieważ kod w procedurach obsługi jest wykonywany asynchronicznie, ze świeżym stosem, nie jest nawet wylogowywany do konsoli. Naprawmy to:
function logAndTossAgain(toss) { var tossWord = tossTable[toss]; console.log("Tossed a " + tossWord.toUpperCase() + "."); } function logErrorMessage(error) { console.log("Oops: " + error.message); } toss() .then(logAndTossAgain) .then(logAndTossAgain) .then(logAndTossAgain) .then(null, logErrorMessage);
Uruchomienie powyższego kodu powoduje teraz wyświetlenie błędu:
"Tossed a TWO." "Oops: Cannot read property 'toUpperCase' of undefined"
Zapomnieliśmy zwrócić coś z logAndTossAgain
i druga obietnica została spełniona z undefined
. Następny program obsługi realizacji zamówienia wybucha, próbując zadzwonić toUpperCase
w tej sprawie. To kolejna ważna rzecz do zapamiętania: zawsze zwracaj coś od handlerów lub bądź przygotowany na kolejne handlery, aby nic nie przeszło.
Budynek wyższy
Zapoznaliśmy się już z podstawami obietnic JavaScript w przykładowym kodzie tego samouczka. Wielką zaletą korzystania z nich jest to, że można je skomponować w prosty sposób, aby stworzyć „złożone” obietnice o zachowaniu, które byśmy chcieli. Biblioteka rsvp.js
zapewnia ich garść i zawsze możesz stworzyć własną, używając prymitywów i tych wyższego poziomu.
Ostatnim, najbardziej złożonym przykładem jest podróż do świata RPG AD&D i rzucanie kostką, aby uzyskać wyniki postaci. Takie wyniki uzyskuje się rzucając trzema kośćmi za każdą umiejętność postaci.
Pozwól mi najpierw wkleić kod tutaj, a następnie wyjaśnię, co nowego:
function toss() { var n = Math.floor(Math.random() * 6) + 1; return new RSVP.resolve(n); // [1] } function threeDice() { var tosses = []; function add(x, y) { return x + y; } for (var i=0; i<3; i++) { tosses.push(toss()); } return RSVP.all(tosses).then(function(results) { // [2] return results.reduce(add); // [3] }); } function logResults(result) { console.log("Rolled " + result + " with three dice."); } function logErrorMessage(error) { console.log("Oops: " + error.message); } threeDice() .then(logResults) .then(null, logErrorMessage);
Znamy toss
z ostatniego przykładu kodu. Po prostu tworzy obietnicę, która zawsze spełnia się w wyniku rzucenia kostką. Użyłem RSVP.resolve
, wygodnej metody, która tworzy taką obietnicę przy mniejszej ceremonii (patrz [1] w powyższym kodzie).
W threeDice
stworzyłem 3 obietnice, z których każda reprezentuje rzut kostką, i ostatecznie połączyłem je z RSVP.all
. RSVP.all
pobiera tablicę obietnic i jest rozwiązywany za pomocą tablicy ich rozwiązanych wartości, po jednej dla każdej obietnicy składowej, z zachowaniem ich kolejności. Oznacza to, że mamy wynik rzutów w results
(patrz [2] w powyższym kodzie) i zwracamy obietnicę, która jest spełniona wraz z ich sumą (patrz [3] w powyższym kodzie).
Po rozwiązaniu otrzymanej obietnicy rejestrowana jest całkowita liczba:
"Rolled 11 with three dice"
Wykorzystywanie obietnic do rozwiązywania rzeczywistych problemów
Obietnice JavaScript są używane do rozwiązywania problemów w aplikacjach, które są znacznie bardziej złożone niż asynchroniczne rzucanie kostką bez powodu .
Jeśli zastąpisz rzucanie trzema kośćmi wysyłaniem trzech żądań ajax do oddzielnych punktów końcowych i kontynuowaniem, gdy wszystkie z nich powrócą pomyślnie (lub jeśli którykolwiek z nich się nie powiódł), masz już użyteczną aplikację obietnic i RSVP.all
.
Obietnice, gdy są używane poprawnie, tworzą łatwy do odczytania kod, który jest łatwiejszy do zrozumienia, a tym samym łatwiejszy do debugowania niż wywołania zwrotne. Nie ma potrzeby ustanawiania konwencji dotyczących np. obsługi błędów, ponieważ są one już częścią specyfikacji.
Ledwie zarysowaliśmy powierzchnię tego, co obietnice mogą zrobić w tym samouczku JavaScript. Biblioteki Promise zapewniają kilkanaście metod i konstruktorów niskiego poziomu, które są do Twojej dyspozycji. Opanuj je, a niebo jest granicą tego, co możesz z nimi zrobić.
O autorze
Balint Erdi był wielkim fanem RPG i AD&D dawno temu, a teraz jest wielką obietnicą i fanem Ember.js. Niezmienna jest jego pasja do rock&rolla. Dlatego postanowił napisać książkę o Ember.js, w której motywem przewodnim aplikacji w książce jest rock & roll. Zarejestruj się tutaj, aby wiedzieć, kiedy zostanie uruchomiony.