Obiekt Deferred ułatwia zarządzanie i obsługę asynchronicznych zadań (wykonywanych po jakimś czasie), jak choćby odbieranie odpowiedzi poprzez AJAX, obsługę wielu animacji czy organizację kodu obsługującego zdarzenia interfejsu aplikacji.
Celem obiektu Deferred oraz obietnic (deferred.promise) jest tworzenie prostego, łatwego do czytania kodu czy odseparowanie logiki aplikacji od funkcji obsługujących zdarzenia interfejsu.
W poprzednim wpisie opisałem jak uprościć sterowanie JavaScriptem z użyciem zdarzeń. Ten wpis jest kontynuacją serii, w której prezentuję sposoby radzenia sobie z asynchronicznymi, nieblokującymi interfejs, wykonywanymi w czasie zadaniami.
Obiekty Deferred i Promise znajdziemy m. in. w jQuery czy w Dojo. W niniejszym wpisie posłużę się implementacją znaną z jQuery.
Czym są obietnice oraz jQuery.Deferred()?
jQuery.Deferred() to obiekt dodany do jQuery w wersji 1.5, czyli dawno temu. Do dzisiaj jednak jest on dość rzadko wykorzystywany przez innych developerów (wniosek z pobieżnych obserwacji), a szkoda pomijać tak świetną funkcjonalność :-)
Obiekt ten służy do radzenia sobie z funkcjami wykonywanymi z czasowym opóźnieniem, jak choćby żądaniami AJAX. Najczęściej spotykanym sposobem jest jednak ten na wzór poniższego:
1 2 3 4 5 6 7 8 9 10 11 12 | jQuery.ajax({ 'url': 'somefile.html', 'type': 'GET', 'success': function(html) { jQuery(html).appendTo(container).hide().fadeIn(1500, function() { jQuery(this).fadeOut(500, function() { jQuery(this).remove(); console.log('game over'); }); }); } }); |
Jak więc widzisz, szybko doprowadziłbyś do miliona funkcji zwrotnych i kłopotliwych ich zagnieżdżeń. jQuery.Deferred pomoże Ci uniknąć zagnieżdżonych funkcji zwrotnych.
Obiekt Deferred pomoże Ci wyjść z powyższej sytuacji w bardzo prosty sposób:
1 2 3 4 5 6 7 8 9 10 11 | jQuery.when(jQuery.ajax({ 'url': 'somefile.html', 'type': 'GET' })).then(function(html) { return jQuery(html).appendTo(container).hide().fadeIn(1500); }).then(function(element) { return element.fadeOut(500); }).done(function(element) { element.remove(); console.log('game over'); }); |
Wprowadzenie do obiektu jQuery.Deferred
Już po części zobaczyłeś możliwości obiektu Deferred w jQuery, czas więc dokładniej przedstawić Twojego nowego sprzymierzeńca :-) jQuery.Deferred ma dwie szczególnie ważne metody:
- resolve([args]) – zgłasza zadanie jako wykonane prawidłowo,
- reject([args]) – przerwy zadanie zakończone niepowodzeniem.
Jest też kilka dodatkowych metod do odbierania funkcji zwrotnych z obiektu Deferred:
- then(doneFilter, failFilter, progressFilter) – dodaje dodatkowe funkcje zwrotne dla obiektu Deferred, w zależności od zgłoszonego stanu,
- done(doneCallbacks) – metoda ta wywoływana jest po prawidłowo wykonanej obietnicy, czyli w momencie wywołania Deferred.resolve,
- fail(failCallbacks) – ma podobne działanie jak metoda done, lecz jest wywoływana w przypadku niewykonania obietnicy (Deferred.reject),
- aways(alwaysCallbacks) – wywoływana po skończonej obietnicy, niezależnie od rezultatu (resolve/reject).
Działanie obiektu Deferred jest banalnie proste, co ukazuje poniższy przykład, w którym po 1 sekundzie w konsoli przeglądarki pojawi się napis „mission complete!”:
1 2 3 4 5 6 7 8 | var deferred = jQuery.Deferred(); deferred.done(function(value) { console.log(value); }); window.setTimeout(function() { deferred.resolve('mission complete!'); }, 1000); |
Obiekt Deferred może zostać również użyty dla obiektu jQuery.ajax:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | var deferred = jQuery.Deferred(); jQuery.ajax({ 'url': 'somefile.html', 'type': 'GET', 'success': deferred.resolve, 'error': deferred.reject }); deferred.done(function(html) { jQuery(html).appendTo(container); }); deferred.fail(function() { console.log('error'); }); |
Na szczęście w jQuery wbudowano obiekt Deferred w miejscach, w których funkcje zwrotne wykonywane są po czasie (np. funkcje animacji, do obsługi AJAX-u), co upraszcza pracę.
Jak użyć w praktyce Deferred.promise?
Zwrócenie metody Deferred.promise() informuje, że operacja będzie wykonana po jakimś czasie. Przydaje się więc przy definiowaniu własnych obietnic, jak choćby:
1 2 3 4 5 6 7 8 9 10 11 12 13 | function runApp() { var deferred = jQuery.Deferred(); window.setTimeout(function() { return deferred.resolve('done!'); }, 1000); return deferred.promise(); }; jQuery.when(runApp()).done(function(value) { console.log(value); }); |
Deferred.promise doskonale nadaje się również do usprawnienia obsługi animacji w jQuery. Możemy dzięki temu łatwiej zarządzać animacjami, obserwować ich postęp czy wywoływać w odpowiednim momencie funkcje zwrotne.
1 2 3 4 5 6 | var h1 = jQuery('h1'); h1.fadeOut(500).fadeIn(1000).animate({'font-size': 125}, 500); h1.promise().done(function() { console.log('Finished!'); }); |
Metoda Deferred.promise jest także wykorzystywana w wielu miejscach silnika jQuery, jak choćby przy wcześniej prezentowanej metodzie do obsługi AJAX-a.
Metoda Deferred.pipe
Bardzo użyteczna metoda mająca za cel filtrować dane pochodzące z obiektu Deferred. Aby lepiej to zobrazować, posłużmy się przykładem rozszerzającym poprzedni:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | var deferred = jQuery.Deferred(); jQuery.ajax({ 'url': 'somefile.html', 'type': 'GET', 'success': deferred.resolve, 'error': deferred.reject }); var filtered = deferred.pipe(function(html) { return jQuery(html).find('>span'); }); filtered.done(function(html) { html.appendTo(container); }); |
Jak więc widzisz, dzięki metodzie pipe możesz wykonać różne operacje na danych, w tym także takie, które nie zwracają obiektu Deferred i nie wykonują się asynchronicznie, a mimo to muszą być wykonane dopiero we właściwym momencie. To bardzo przydatna funkcjonalność, zapewne przekonasz się w praktyce! :-)
Łączenie obietnic z jQuery.when
Omówiłem już obietnice (Deferred.promise), zaprezentowałem w jednym z przykładów jQuery.when – czas połączyć zebraną wiedzę w jednym przykładzie:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | function runApp() { var deferred = jQuery.Deferred(); window.setTimeout(function() { return deferred.resolve('done!'); }, 1000); return deferred.promise(); }; function getFile() { return jQuery.ajax({ 'url': 'somefile.html', 'type': 'GET' }); }; jQuery.when(runApp(), getFile()).done(function(valueA, valueB) { console.log('runApp: ', valueA); console.log('getFile: ', valueB); }); |
W powyższym przykładzie wywołujemy dwie różne funkcje z asynchronicznym kodem, które wykonają się po jakimś czasie. Dopiero po ich wykonaniu nastąpi funkcja zwrotna, która w konsoli wypisze zwrócone wyniki.
Obietnice czy zdarzenia?
W ostatnim wpisie przedstawiłem bardzo ciekawą i prostą w obsłudze technikę do sterowania flow w interfejsie klienta – niestandardowe zdarzenia. Dzisiejszy temat był nieco podobny i w wielu przypadkach może stanowić dla poprzedniego alternatywę.
Oba rozwiązania są proste i użyteczne, umożliwiają lepsze oddzielnie funkcji obsługujących interfejs użytkownika od logiki aplikacji, jak również pozwalają uciec od zagnieżdżonych funkcji zwrotnych. Do Ciebie należy wybór, którą metodę wybierzesz, choć niewykluczone jest stosowaniu obu jednocześnie, co często też robię :-).
Jeśli dobrze kojarzę specyfikację Promises/A, to jQuery błędnie implementuje (a przynajmniej tak robiła jeszcze jakiś czas temu) obietnice, gdyż pozwala zmieniać stan już PO fulfilled i rejected.
Obietnica może przyjmować tylko dwa stany: zwraca wartość lub wyrzuca wyjątek – jeżeli przyjmie stan, nie ma możliwości by zmieniła go.
A w praktyce obietnica może zwrócić wartość lub wyrzucić wyjątek (czyli 4 kombinacje: fulfilled value, fulfilled exception, rejected value, rejected exception).
Obietnice nie służą do radzenia sobie z funkcjami wykonywanymi z czasowym opóźnieniem a do zmiany zapisu procesów asynchroniczny w sposób synchroniczny.
@Michał: nie wiem czy dobrze rozumiem, ale ten przykład nie zmieni stanu już po pierwszej zmianie stanu:
2
3
4
5
6
7
console.log(deferred.state()); // "pending "
deferred.reject();
console.log(deferred.state()); // "rejected"
deferred.resolve();
console.log(deferred.state()); // "rejected"
Chyba, że chodzi o jakiś inny przypadek zmiany stanu lub wcześniejszą wersję jQuery (testowałem na najnowszej).
Widocznie poprawili już, nie jestem z jQuery na bieżąco.
jQuery cały czas źle implementuje obietnice i nie nadaje się do mocniejszych zastosowań.
Pierwszy podstawowy błąd (zwracanie wejściowej obietnicy przez then) szczęśliwie naprawili wraz 1.8, natomiast drugi błąd czyli brak możliwości przejścia w następnej obietnicy w łańcuchu na stan ok, wciąż jest w wersji 2.0
Otwórzcie konsolę i puście to: http://jsfiddle.net/XvMUa/2/
Dobrze też swego czasu to wytłumaczył Domenic Denicola -> https://gist.github.com/domenic/3889970
Jeśli chcielibyście się pobawić obietnicami po stronie serwera, gdzie jest dużo więcej wyzwań na tym polu, to polecam jakąś dedykowaną implementację i nie korzystanie z jQuery
@medikoo: dzięki za wartościowe uzupełnienie/poprawkę do wpisu! Już trochę używałem obietnic w jQuery i miałem szczęście nie trafić na ten błąd, aczkolwiek dobrze wiedzieć, by kiedyś nie dać się zaskoczyć.
Ps. muszę póki co używać jQuery i wspierać starsze IE – standard firmowy.