Hej! Dziś temat, o którym wspomniałem już poprzednio przy okazji ostatniej części serii wpisów dotyczących testowania za pomocą frameworka Jasmine. Pokazywałem tam, w jaki sposób można “zamokować” wywołanie funkcji ajax dostępnej w bibliotece jQuery za pomocą specjalnych obiektów jQuery Deffered oraz Promise. Uznałem wówczas, że szczegóły tego rozwiązania to temat na osobny wpis… i dziś właśnie postanowiłem Wam je w przystępny sposób przekazać (mam nadzieję, że mi się uda…). Zaczynamy!

Główny koncept

Obiekty jQuery Deffered oraz Promise mają najczęściej zastosowanie w przypadku wywołań asynchronicznych AJAX, spójrzmy więc na prosty przykład kodu, który realizuje takie właśnie wywołanie właśnie za pomocą biblioteki jQuery:

$.ajax({
    url: '/actions/get',
    data: {
        index: 1
    },
    type: 'POST',
    success: function(result){
        alert('success with result: ' + result);
    },
    error: function(){
        alert('error');
    }
});

Myślę, że każdy doskonale zna powyższe rozwiązanie - pozwala ono na przekazanie dwóch funkcji wywołania zwrotnego, obsługujących odpowiednio wywołanie AJAX zakończone powodzeniem oraz błędem. Jednakże taki interfejs nie jest standardowy, ponadto takie podejście uniemożliwia rejestrowanie więcej niż jednego “callbacka”.

Jak to zrobić lepiej?

Ano, nadszedł czas na poznanie obiektu deferred. Zobaczmy co na jego temat mówi nam dokumentacja jQuery:

“The Deferred object, introduced in jQuery 1.5, is a chainable utility object created by calling the jQuery.Deferred() method. It can register multiple callbacks into callback queues, invoke callback queues, and relay the success or failure state of any synchronous or asynchronous function.”

Dalszy ciąg tej definicji znajdziecie w dokumentacji dostępnej na stronach biblioteki jQuery. Jak więc widzicie, jest to taki specjalny obiekt, pozwalający na rejestrowanie wielu funkcji wywołania zwrotnego do kolejek takich wywołań. Pozwala też na wywoływanie tych kolejek oraz przekazywanie stanu sukcesu i porażki każdej synchronicznej jak i asynchronicznej operacji.

Przykład najprostszy

No fajnie, definicję już znamy… Lepiej jednak zobaczyć jak to działa w praktyce. Poniżej najprostszy przykład jaki tylko może być ;)

var def = $.Deferred();

def.done(function (result) {
    alert(result);
}).done(function (value){
    alert('no i ' + value);
});

def.resolve('ekstra!');

Jak widać, obiekt deferred tworzymy wywołując specjalny konstruktor dostępny w obiekcie globalnym jQuery. W trzeciej linii widzimy, w jaki sposób można rejestrować funkcje wywołania zwrotnego - w przykładzie rejestrujemy taki callback dla przypadku, w którym operacja się powiedzie. Zauważyliście pewnie, że funkcja done wywoływana jest tutaj dwa razy - właśnie za pomocą takiego łańcucha rejestruje się kolejne callbacki. W ostatniej linii widzimy przykład wywołania takiego łańcucha callbacków - w tym przypadku wywołane zostaną wszystkie funkcje wywołania zwrotnego zarejestrowane za pomocą funkcji done() i zostanie do nich przekazany parametr (lub parametry), które przekazane zostaną do funkcji resolve.

Obsługa błędu

Przykład dla sytuacji błędu poniżej:

var def = $.Deferred();

def.fail(function (result) {
    alert(result);
}).fail(function (value){
    alert('no i ' + value);
});

def.reject('dupa!');

Tak jak widzicie, zamiast done wywołujemy funkcję fail, a zamiast resolve wywołujemy funkcję reject. Nie ma tutaj żadnej większej filozofii. Ok, to przejdźmy do najważniejszej cechy tego obiektu czyli funkcji promise()!

Funkcja promise

Funkcja promise() obiektu deferred, zwraca kolejny specjalny obiekt (nazywany tak samo jak funkcja, która go zwraca). Obiekt ten jest bardzo podobny do obiektu deferred, posiada on jednak tylko funkcje odpowiedzialne za rejestrowanie callbacków, nie pozwala natomiast na kontrolowanie stanu (takie jak przykładowe resolve lub reject). Pozwala on innym na rejestrowanie funkcji wywołania zwrotnego do naszej funkcji ale jednocześnie uniemożliwia kontrolowanie stanu. Zresztą zobaczcie sami jak to działa na przykładzie:

function test () {
    var def = $.Deferred();

    setTimeout(function(){
        def.resolve('ekstra!');
    }, 3000);

    return def.promise();
}

test().done(function(value){
    alert(value);
});

Powyżej mamy definicję funkcji test(), która tworzy obiekt deferred. Jak widzicie, następnie wywoływana jest funkcja resolve - jednak jest ona wywołana z opóźnieniem. Na końcu zwracamy obiekt promise - moglibyśmy tutaj zwrócić po prostu obiekt deferred ale wtedy ktoś, kto wywołałby tę funkcję mógłby ten obiekt zmienić (wywołać na przykład resolve / reject), a tak może tylko rejestrować callbacki.

Na końcu wykorzystujemy nasz kod w praktyce - jako, że test() zwraca obiekt promise, możemy na nim rejestrować callbacki, które obsłużą operację asynchroniczną.

Wykorzystanie z funkcją ajax

No i doszliśmy do finału tego wpisu - otóż funkcja ajax biblioteki jQuery również zwraca obiekt promise, możemy więc z niej korzystać w taki sam sposób! Zobaczmy jak by to mogło wyglądać dla wywołania funkcji ajax z początku wpisu:

var result = $.ajax({
    url: '/actions/get',
    data: {
        index: 1
    },
    type: 'POST'
});

result.done(function (value){
     alert('success with result: ' + value);
});

result.fail(function (value){
    alert('error');
});

jQuery Deffered oraz Promise - podsumowanie

Prawda, że proste? Myślę, że warto zapoznać się dokładniej z obiektami jQuery Deffered oraz Promise - mają trochę więcej możliwości niż opisane w dzisiejszym wpisie :) Polecam doczytać trochę na przykład na temat funkcji pipe() lub o $.when()!