• Strona główna
  • Curriculum Vitae
  • O mnie
  • Przykład: Gramatyka w PHP
  • Przykład: Kompresja CSS
  • Przykład: Kompresja JavaScript
  • Przykład: Skracanie linków
  • Przykład: Wykrywanie serwera HTTP
  • Przykład: Własna bramka SMS
  • Mapa strony
  • Kontakt
Niebieski Pomarańczowy Zielony Różowy Fioletowy

JavaScript sterowany obietnicami

Opublikowane 12 maja 2013. Autor: Kamil Brenk. Wizyt: 4 038.

Kategorie: JavaScript
Tematyka: JavaScript, jQuery

maj 12

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ę :-).

Komentarze (5)

  1. Michał 12 maja 2013

    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.

  2. Kamil Brenk 12 maja 2013

    @Michał: nie wiem czy dobrze rozumiem, ale ten przykład nie zmieni stanu już po pierwszej zmianie stanu:

    1
    2
    3
    4
    5
    6
    7
    var deferred = jQuery.Deferred();

    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).

  3. Michał 14 maja 2013

    Widocznie poprawili już, nie jestem z jQuery na bieżąco.

  4. medikoo 23 maja 2013

    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

  5. Kamil Brenk 23 maja 2013

    @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.



Kamil Brenk Blog

PHP, JavaScript, SQL, HTML

  • Informacje o blogu

    Kamil Brenk

    Blog o tworzeniu aplikacji na potrzeby sieci Web.

    Praktyczne przykłady, porady i sztuczki. PHP, SQL, AJAX, JavaScript, HTML i pochodne.

    Kanał RSS

    • Najnowsze
    • Komentarze
    • Popularne
    • Liczniki w CSS
    • Wyprzedaż książek o programowaniu!
    • Niestandardowy placeholder
    • JavaScript w modułach
    • Co dalej z blogiem?
    • Interaktywna mapa w HTML i CSS
    • Olsztyn: Jak wyseparować zawartość zassaną przez file_get_content?
    • ERMLAB: Od czegoś trzeba zacząć :) Wiele osób właśnie stawia na...
    • david: co nalezy wkleić na stronę aby plik ze stylami był ladowany...
    • krynicz: Nie jestem pewien czy dobrze to rozumiem: wpisujemy OG w...
    • yaro: Jak zmienić re_write znak "_" na "-"?
    • Piotr: stworzyłem prostą stronkę w PHP, czy jest możliwość aby...
    • MichalR: Super sprawa... bardzo przydatne.. dzieki i pozdrawiam..
    • Niestandardowe czcionki na stronie
    • Sposoby wczytywania JavaScript
    • Gramatyka w PHP, część 1
    • Umowa i zaliczka dla freelancera
    • Wysyłanie wiadomości SMS w PHP
    • Projekt aplikacji po stronie klienta
    • Własny mechanizm Feed
  • Szukajka
    Wpisz co chcesz wyszukać na stronie…
  • Kategorie
    • Apache
    • Freelancer
    • Front-end Development
    • HTML5 & CSS3
    • Inne
    • JavaScript
    • Książki
    • PHP
    • Po godzinach
    • Pozycjonowanie
    • Protokół HTTP
    • SQL
    • Wyrażenia regularne
  • Moje serwisy
    • Testy zawodowe
    • Miłość, uczucia i seks
  • Czytane blogi
    • Wojciech Sznapka
    • Wojciech Soczyński
    • Michał Wachowski
    • Tomasz Kowalczyk
    • Filip Górczyński
  • Strona główna
  • Curriculum Vitae
  • O mnie
  • Przykład: Gramatyka w PHP
  • Przykład: Kompresja CSS
  • Przykład: Kompresja JavaScript
  • Przykład: Skracanie linków
  • Przykład: Wykrywanie serwera HTTP
  • Przykład: Własna bramka SMS
  • Mapa strony
  • Kontakt

Kamil Brenk © 2010. All rights reserved.

Designed by FTL Wordpress Themes brought to you by Smashing Magazine.

Do góry ∧