Comprensione di jQuery.Deferred e Promise

Indice

In questo post ci sono molti esempi eseguibili in Javascript, ma potrebbe non funzionare su un lettore RSS.
JQuery 1.5 aveva introdotto il concetto di “azioni differite“. Trovo questo concetto molto potente quando si lavora con Javascript e AJAX, e penso che sia un punto di svolta per il modo in cui siamo abituati a scrivere il codice asincrono in js.
Di solito, il modo in cui siamo abituati a trattare con codice asincrono in Javascript è passando un callback come argomento alla funzione:

Copia codice

#
jQuery.ajax({
    url: "/echo/json/",
    data: {
    json: JSON.stringify({
       firstName: "Pippo",
       lastName: "Cani"})} ,
    type: "POST",
    success: function(person){
        alert(person.firstName + " salvata.");
    },
    error: function(){
        alert("error!");
    }
});
#
Come potete vedere qui stiamo passando due callback (success ed error

) al metodo jQuery.Ajax utilizzando la sintassi dell’oggetto-letterale supportato dal metodo.
Questo lavoro, che non è un’interfaccia standard, richiede una modifica all’interno del metodo post e non si può registrare dei callback multipli.

Presentazione di jQuery.Deferred
jQuery 1.6 apporta due nuovi metodi per l’uso degli oggetti differiti:
il primo è il metodo always()che può essere utilizzato per eseguire una funzione indipendentemente dal fatto che l’oggetto differito sia risolto o rifiutato;
il secondo è il nuovo metodo pipe(), che può essere utilizzato per filtrare gli oggetti passivi sia quando si risolvono o quando falliscono.

Il metodo restituisce un valore filtrato che può essere passato al metodo done() o fail() dell’oggetto differito o al nuovo oggetto promise.
L’oggetto differito è abbastanza facile da capire ma potente. Ha due metodi importanti:

  • resolve
  • reject

E ha tre importanti “eventi” o modi di allegare una retrorichiamata:

  • done
  • fail
  • always

così, in un esempio fondamentalmente stupido sarà qualcosa del genere:

Copia codice

#
var deferred = jQuery.Deferred();
deferred.done(function(value) {
   alert(value);
});
deferred.resolve("Ciao Mondo");;
#
Se si chiama il metodo “reject“, i falliti callback collegati verranno eseguite, la richiamata viene sempre eseguita a prescindere se l’azione differita sia risolta o rifiutata.
Un altro punto interessante è che se si allega una richiamata ad una differita già risolta, si ottiene che viene eseguito immediatamente:
Copia codice

#
var deferred = jQuery.Deferred();
deferred.resolve("Ciao Mondo");
deferred.done(function(value) {
    alert(value);
});
#
Il metodo Promise()
Il nuovo metodo promise() restituisce l’oggetto promise che può essere utilizzato per eseguire del codice arbitrario una volta che tutte le azioni di ogni elemento della collezione, collegate al metodo, siano state completate. Per impostazione predefinita l’oggetto promise monitora le code fx; ma le code personalizzate possono essere specificate fornendo un parametro opzionale al metodo. Un oggetto con il metodo promise() collegato, può essere specificato usando un secondo argomento opzionale per evitare di creare un nuovo oggetto.
Il metodo può essere usato in congiunzione con metodi come done() per eseguire una funzione, quando tutte le animazioni su un elemento sono state completate:
Copia codice

#
$("#animated").promise().done(function () {
    //fai qualcosa
});
#
L’oggetto Deferred ha un altro metodo importante di nome Promise(). Questo metodo restituisce un oggetto con quasi la stessa interfaccia che il Deferred, ma solo ha i metodi da attaccare ai callback e non ha i metodi per “resolve” e “reject“.
Questo è utile quando si vuole fare una chiamata a una API per trarre uso, ma non ha la capacità di risolvere o rifiutare il differito. Questo codice non riuscirà perché la promise non ha un metodo “resolve“:
Copia codice

#
function getPromise(){
    return jQuery.Deferred().promise();
}
try{
    getPromise().resolve("a");
}
catch(err){
    alert(err);
}
#
Il metodo jQuery.ajax in JQuery restituisce una Promise, in modo da poter fare:
Copia codice

#
var post = jQuery.ajax({
    url: "/echo/json/",
    data: {
        json: JSON.stringify({
            firstName: "Pippo",
            lastName: "Cani"
    })} ,
    type: "POST"
});
post.done(function(p){
    alert(p.firstName +  " salvato.");
});
post.fail(function(){
    alert("error!");
});
#
Questo codice fa lo stesso che il frammento di prima con la sola differenza che è possibile aggiungere i callback che si vuole, la sintassi è chiara perché non abbiamo bisogno di un parametro in più nel metodo. D’altra parte è possibile utilizzare la promessa come valore di ritorno di altra funzione, e una delle cose più interessanti è che si possono fare delle operazioni con le promesse.
Il metodo Pipe()
Il metodo Pipe è molto potente e utile, permette di “progettare” una promessa.
Seguendo l’esempio precedente, possiamo fare qualcosa del genere:
Copia codice

#
var post = jQuery.post(
    "/echo/json/",
    {json: JSON.stringify({
            firstName: "Pippo",
            lastName: "Cani"
        })
    }).pipe(function(p){
        return "Salvato " + p.firstName;
});
post.done(function(r){
    alert(r);
})
#
Qui stiamo facendo una proiezione dei risultati che sarebbe un oggetto “person“. Così, invece di avere una differita di person, abbiamo ora una differita di “Salvato {firstName}“.
Un’altra caratteristica molto interessante del metodo pipe è che può restituire un differito dall’interno del pipe di callback. Immaginiamo di avere due metodi, uno per ottenere un ID di un Cliente attraverso un id interno e un altro per ottenere l’indirizzo di una persona dal ID:
Copia codice

#
function getCustomerById(customerId){
    return jQuery.post("/echo/json/", {
        json: JSON.stringify({
            firstName: "Pippo",
            lastName: "Cani",
            ID: "123456789"
        })
    }).pipe(function(p){
        return p.ID;
    });
}
function getPersonAddressById(ID){
    return jQuery.post("/echo/json/", {
        json: JSON.stringify({
            ID: "123456789",
            address: "Sempre Viva 12345, Legione Straniera"
        })
    }).pipe(function(p){
        return p.address;
    });
}
function getPersonAddressById(id){
    return getCustomerById(id)
        .pipe(getPersonAddressById);
}
getPersonAddressById(123)
    .done(function(a){
        alert("L'indirizzo è " + a);
    });
#
Come potete vedere qui, ho aggiunto un nuovo metodo getPersonAddressByID. Questo metodo restituisce una differita che è una combinazione dei due metodi. Se uno dei metodi della pipeline non supera il “master” differito allora fallirà.
Le pipelines presentano alcuni altri usi, per esempio è possibile rifiutare la differita all’interno del callback del pipe.
Un altro caso interessante del uso che mi viene in mente per i pipes sono le differite ricorsive. Immagina che hai iniziato un’operazione asincrona nel backend, ed è necessario fare il “polling” per vedere se il compito è fatto e, quando il compito è fatto, fare qualcosa di diverso.
Copia codice

#
//1: fatto, 2: cancellato, other: attesa
function getPrintingStatus(){
    var d = jQuery.Deferred();
    jQuery.post(
        "/echo/json/",
        {
            json: JSON.stringify({
		status: Math.floor(Math.random()*8+1)
	    }),
            delay: 2
        }
    ).done(function(s){
        d.resolve(s.status);
    }).fail(d.reject);
    return d.promise();
}
function pollUntilDone(){
    //fare qualcosa
    return getPrintingStatus()
            .pipe(function(s){
                if(s === 1 || s == 2) {
                    // se lo status è fatto o cancellato
                    // restituisce lo status
                    return s;
                }
                // se lo status è in attesa ...
                // chiama la stessa funzione ...
                // e restituisce un differito ...
                return pollUntilDone();
            });
}
jQuery.blockUI({message: "Caricando..."});
pollUntilDone()
    .pipe(function(s){
        // progetto il codice di stato
        // in una stringa di senso compiuto.
        switch(s){
            case 1:
                return "fatto";
            case 2:
                return "cancellato";
        }
    })
    .done(function(s){
        jQuery.unblockUI();
        alert("Lo status è " + s);
    });
#
Combinando le promesse con jQuery.when
Un altro metodo molto utile è di jQuery.when. Il metodo accetta un numero arbitrario di promesse, e restituisce un master differito che:

  • sarà “resolved” quando tutte le promesse saranno risolte:
  • sarà “rejected” se una delle promesse sarà rifiutata

La richiamata ha i risultati di tutte le promesse.
Questo è un esempio:

Copia codice

#
function getCustomer(customerId){
    var d = jQuery.Deferred();
    jQuery.post(
        "/echo/json/",
        {json: JSON.stringify({
            firstName: "Pippo",
            lastName: "Cani",
            ID: "123456789"})
        }).done(function(p){
        d.resolve(p);
    }).fail(d.reject);
    return d.promise();
}
function getPersonAddressById(ID){
    return jQuery.post(
            "/echo/json/",
            {json: JSON.stringify({
                 ID: "123456789",
                 address: "Siempre Viva 12345, Legione Straniera"
            })
        }).pipe(function(p){
            return p.address;
        });
}
jQuery.when(getCustomer(123), getPersonAddressById("123456789"))
    .done(function(person, address){
        alert("Il nome è " + person.firstName + " e il suo indirizzo è " + address);
    });
#
Come si può vedere alla fine di questo esempio, jQuery.when restituisce una nuova differita e stiamo usando due risultati èer callback realizzato.

Notate che ho cambiato il metodo GetCustomer; questo perché nella promessa di una chiamata AJAX, il carico utile è contenuto come primo elemento nel risultato, ma ha anche altre cose come il codice di stato.

In questo esempio si possono mescolare jQuery.when e pipes come segue:

Copia codice

#
function getCustomer(customerId){
    var d = jQuery.Deferred();
    jQuery.post(
        "/echo/json/",
        {json: JSON.stringify({
            firstName: "Pippo",
            lastName: "Cani",
            ID: "123456789"
        })
    }).done(function(p){
        d.resolve(p);
    }).fail(d.reject);
    return d.promise();
}
function getPersonAddressById(ID){
    return jQuery.post(
        "/echo/json/", {
        json: JSON.stringify({
            ID: "123456789",
            address: "Siempre Viva 12345, Legione Straniera"
        })
    }).pipe(function(p){
        return p.address;
    });
}
jQuery.when(getCustomer(123), getPersonAddressById("123456789"))
    .pipe(function(person, address){
        return jQuery.extend(person, {address: address});
    }).done(function(person){
        alert("Il nome è " + person.firstName + " e il suo indirizzo è " + person.address);
    });
#
Un altro caso interessante con l’operatore jQuery.when, è quando è necessario caricare parecchie cose sullo schermo, ma si vuole solo un unico messaggio per il loading:
Copia codice

#
function getCustomer(customerId){
    var d = jQuery.Deferred();
    jQuery.post(
        "/echo/json/",
        {json: JSON.stringify({
            firstName: "Pippo",
            lastName: "Cani",
            ID: "123456789"
        }),
        delay: 4
    }).done(function(p){
        d.resolve(p);
    }).fail(d.reject);
    return d.c;
}
function getPersonAddressById(ID){
    return jQuery.post(
        "/echo/json/", {
        json: JSON.stringify({
            ID: "123456789",
            address: "Siempre Viva 12345, Legione Straniera"
        }),
        delay: 2
    }).pipe(function(p){
        return p.address;
    });
}
function load(){
    jQuery.blockUI({message: "Caricando..."});
    var loadingCustomer = getCustomer(123)
        .done(function(c){
             $("span#firstName").html(c.firstName)
        });
    var loadingAddress = getPersonAddressById("123456789")
        .done(function(address){
            $("span#address").html(address)
        });
    jQuery.when(loadingCustomer, loadingAddress)
        .done(jQuery.unblockUI);
}
load();
#
Ci sono altri metodi utili e modi in cui è possibile combinare i differiti e incoraggio vivamente di leggere il suo uso nella documentazione di jQuery su Deferred Object.
Questo è tutto per ora! Spero che questo post sia utile, come lo è per me a portata di click.