Come si può utilizzare jQuery differito?


279

jQuery 1.5 porta il nuovo oggetto differite ed i metodi associati .when, .Deferrede ._Deferred.

Per coloro che non hanno mai usato .Deferredprima, ho annotato la fonte per questo .

Quali sono i possibili usi di questi nuovi metodi, come possiamo adattarli ai modelli?

Ho già letto l' API e la fonte , quindi so cosa fa. La mia domanda è: come possiamo usare queste nuove funzionalità nel codice di tutti i giorni?

Ho un semplice esempio di una classe di buffer che chiama la richiesta AJAX in ordine. (Il prossimo inizia dopo quello precedente).

/* Class: Buffer
 *  methods: append
 *
 *  Constructor: takes a function which will be the task handler to be called
 *
 *  .append appends a task to the buffer. Buffer will only call a task when the 
 *  previous task has finished
 */
var Buffer = function(handler) {
    var tasks = [];
    // empty resolved deferred object
    var deferred = $.when();

    // handle the next object
    function handleNextTask() {
        // if the current deferred task has resolved and there are more tasks
        if (deferred.isResolved() && tasks.length > 0) {
            // grab a task
            var task = tasks.shift();
            // set the deferred to be deferred returned from the handler
            deferred = handler(task);
            // if its not a deferred object then set it to be an empty deferred object
            if (!(deferred && deferred.promise)) {
                deferred = $.when();
            }
            // if we have tasks left then handle the next one when the current one 
            // is done.
            if (tasks.length > 0) {
                deferred.done(handleNextTask);
            }
        }
    }

    // appends a task.
    this.append = function(task) {
        // add to the array
        tasks.push(task);
        // handle the next task
        handleNextTask();
    };
};

Sto cercando dimostrazioni e possibili usi di .Deferrede .when.

Sarebbe anche bello vedere esempi di ._Deferred.

Il collegamento alla nuova jQuery.ajaxfonte per esempio è barare.

Sono particolarmente interessato a quali tecniche sono disponibili quando astraggiamo se un'operazione viene eseguita in modo sincrono o asincrono.


19
Dalle FAQ: evita di porre domande soggettive in cui ... ogni risposta è ugualmente valida: "Qual è il tuo ______ preferito?" (la loro enfasi)
TJ Crowder

2
@TJCrowser Vedrò di riformularlo.
Raynos,

5
E 'una buona domanda, ma non ci può essere che molte persone che possono rispondere :-)
a punta

2
@Pointy Ho cercato principalmente coloro che lo usavano quando era un plugin di terze parti. E incoraggiando le persone a sedersi e usarlo!
Raynos,

1
._Deferredè semplicemente il vero "oggetto differito" che .Deferredutilizza. È un oggetto interno che molto probabilmente non ti servirà mai.
David Tang,

Risposte:


212

Il miglior caso d'uso che mi viene in mente è nella memorizzazione nella cache delle risposte AJAX. Ecco un esempio modificato dal post introduttivo di Rebecca Murphey sull'argomento :

var cache = {};

function getData( val ){

    // return either the cached value or jqXHR object wrapped Promise
    return $.when(
        cache[ val ] || 
        $.ajax('/foo/', {
            data: { value: val },
            dataType: 'json',
            success: function( resp ){
                cache[ val ] = resp;
            }
        })
    );
}

getData('foo').then(function(resp){
    // do something with the response, which may
    // or may not have been retrieved using an
    // XHR request.
});

Fondamentalmente, se il valore è già stato richiesto una volta prima di essere restituito immediatamente dalla cache. Altrimenti, una richiesta AJAX recupera i dati e li aggiunge alla cache. Il $.when/ .thennon importa nulla di tutto questo; tutto ciò di cui devi preoccuparti è usare la risposta, che viene passata al .then()gestore in entrambi i casi. jQuery.when()gestisce una non promessa / differita come completata, eseguendo immediatamente qualsiasi .done()o .then()sulla catena.

I differiti sono perfetti per quando l'attività può o meno funzionare in modo asincrono e si desidera sottrarre tale condizione dal codice.

Un altro esempio del mondo reale usando l' $.whenhelper:

$.when($.getJSON('/some/data/'), $.get('template.tpl')).then(function (data, tmpl) {

    $(tmpl) // create a jQuery object out of the template
    .tmpl(data) // compile it
    .appendTo("#target"); // insert it into the DOM

});

4
Due esempi brillanti. Ho implementato qualcosa di simile al secondo, ma con 4 richieste ajax, e si comporta bene, oltre ad essere molto più leggibile, compatto, logico, gestibile, ecc. JQuery.Deferred è davvero una buona cosa.
PJP

5
Ecco un utile video su questo argomento bigbinary.com/videos/3-using-deferred-in-jquery
Nick Vanderbilt

5
La memorizzazione nella cache non funzionerà se il risultato è un valore errato. Inoltre, non mi piace il fatto che getData restituisca 2 tipi diversi a seconda del ramo preso.
Marko Dumic,

3
Vedi la risposta di Julian D. di seguito per una migliore implementazione della cache ajax.
event_jr

1
Non capisco come funziona anche il primo esempio di codice: capisco il caso in cui l'oggetto non è memorizzato nella cache, ma se non lo è, cache[ val ]NON restituirà una promessa (la documentazione di jquery afferma che il parametro è i dati restituiti dal mittente), il che significa che l'accesso del membro .thensarà errore ... giusto? Cosa mi sto perdendo?
Chacham15,

79

Ecco un'implementazione leggermente diversa di una cache AJAX come nella risposta di ehynd .

Come notato nella domanda di follow-up di fortuneRice, l'implementazione di ehynd in realtà non ha impedito molteplici richieste identiche se le richieste sono state eseguite prima che una di esse fosse restituita. Questo è,

for (var i=0; i<3; i++) {
    getData("xxx");
}

molto probabilmente comporterà 3 richieste AJAX se il risultato per "xxx" non è già stato memorizzato nella cache in precedenza.

Questo può essere risolto memorizzando nella cache i Deferreds della richiesta anziché il risultato:

var cache = {};

function getData( val ){

    // Return a promise from the cache (if available)
    // or create a new one (a jqXHR object) and store it in the cache.
    var promise = cache[val];
    if (!promise) {
        promise = $.ajax('/foo/', {
            data: { value: val },
            dataType: 'json'
        });
        cache[val] = promise;
    }
    return promise;
}

$.when(getData('foo')).then(function(resp){
    // do something with the response, which may
    // or may not have been retreived using an
    // XHR request.
});

1
Penso che questo non sia ancora perfetto, dal momento che non hai mai cancellato / aggiornato la cache una volta recuperato. Ciò renderà la chiamata AJAX non funzionante per nessun aggiornamento.
zyzyis,

45

Un differito può essere utilizzato al posto di un mutex. Questo è essenzialmente lo stesso degli scenari multipli di utilizzo di Ajax.

MUTEX

var mutex = 2;

setTimeout(function() {
 callback();
}, 800);

setTimeout(function() {
 callback();
}, 500);

function callback() {
 if (--mutex === 0) {
  //run code
 }
}

DIFFERITA

function timeout(x) {
 var dfd = jQuery.Deferred();
 setTimeout(function() {
  dfd.resolve();
 }, x);
 return dfd.promise();
}

jQuery.when(
timeout(800), timeout(500)).done(function() {
 // run code
});

Quando si utilizza un Deferred solo come mutex, fare attenzione agli impatti sulle prestazioni (http://jsperf.com/deferred-vs-mutex/2). Anche se la convenienza, oltre ai vantaggi aggiuntivi forniti da un rinviato, ne vale la pena e nell'uso effettivo (basato sugli eventi dell'utente) l'impatto sulle prestazioni non dovrebbe essere evidente.


È stato sorprendentemente difficile per me trovarlo. L'ho usato su una funzione contenente un setInterval che restituiva la promessa risolta e si autodistruggeva una volta che l'ampiezza del div superava un certo numero. Era per la risoluzione dei problemi e una soluzione se non riuscivo a risolvere il mio problema, ma sono entusiasta.
JSG,


20

Un altro uso che sto mettendo a frutto è il recupero di dati da più fonti. Nell'esempio seguente, sto recuperando più oggetti di schema JSON indipendenti utilizzati in un'applicazione esistente per la convalida tra un client e un server REST. In questo caso, non voglio che l'applicazione sul lato browser inizi a caricare i dati prima che vengano caricati tutti gli schemi. $ .when.apply (). then () è perfetto per questo. Grazie a Raynos per i suggerimenti sull'utilizzo di then (fn1, fn2) per monitorare le condizioni di errore.

fetch_sources = function (schema_urls) {
    var fetch_one = function (url) {
            return $.ajax({
                url: url,
                data: {},
                contentType: "application/json; charset=utf-8",
                dataType: "json",
            });
        }
    return $.map(schema_urls, fetch_one);
}

var promises = fetch_sources(data['schemas']);
$.when.apply(null, promises).then(

function () {
    var schemas = $.map(arguments, function (a) {
        return a[0]
    });
    start_application(schemas);
}, function () {
    console.log("FAIL", this, arguments);
});     

10

Un altro esempio che utilizza Deferreds per implementare una cache per qualsiasi tipo di calcolo (in genere alcune attività ad alte prestazioni o di lunga durata):

var ResultsCache = function(computationFunction, cacheKeyGenerator) {
    this._cache = {};
    this._computationFunction = computationFunction;
    if (cacheKeyGenerator)
        this._cacheKeyGenerator = cacheKeyGenerator;
};

ResultsCache.prototype.compute = function() {
    // try to retrieve computation from cache
    var cacheKey = this._cacheKeyGenerator.apply(this, arguments);
    var promise = this._cache[cacheKey];

    // if not yet cached: start computation and store promise in cache 
    if (!promise) {
        var deferred = $.Deferred();
        promise = deferred.promise();
        this._cache[cacheKey] = promise;

        // perform the computation
        var args = Array.prototype.slice.call(arguments);
        args.push(deferred.resolve);
        this._computationFunction.apply(null, args);
    }

    return promise;
};

// Default cache key generator (works with Booleans, Strings, Numbers and Dates)
// You will need to create your own key generator if you work with Arrays etc.
ResultsCache.prototype._cacheKeyGenerator = function(args) {
    return Array.prototype.slice.call(arguments).join("|");
};

Ecco un esempio dell'uso di questa classe per eseguire alcuni calcoli (simulati pesanti):

// The addingMachine will add two numbers
var addingMachine = new ResultsCache(function(a, b, resultHandler) {
    console.log("Performing computation: adding " + a + " and " + b);
    // simulate rather long calculation time by using a 1s timeout
    setTimeout(function() {
        var result = a + b;
        resultHandler(result);
    }, 1000);
});

addingMachine.compute(2, 4).then(function(result) {
    console.log("result: " + result);
});

addingMachine.compute(1, 1).then(function(result) {
    console.log("result: " + result);
});

// cached result will be used
addingMachine.compute(2, 4).then(function(result) {
    console.log("result: " + result);
});

La stessa cache sottostante potrebbe essere utilizzata per memorizzare nella cache le richieste Ajax:

var ajaxCache = new ResultsCache(function(id, resultHandler) {
    console.log("Performing Ajax request for id '" + id + "'");
    $.getJSON('http://jsfiddle.net/echo/jsonp/?callback=?', {value: id}, function(data) {
        resultHandler(data.value);
    });
});

ajaxCache.compute("anID").then(function(result) {
    console.log("result: " + result);
});

ajaxCache.compute("anotherID").then(function(result) {
    console.log("result: " + result);
});

// cached result will be used
ajaxCache.compute("anID").then(function(result) {
    console.log("result: " + result);
});

Puoi giocare con il codice sopra in questo jsFiddle .


9

1) Usalo per garantire un'esecuzione ordinata di callback:

var step1 = new Deferred();
var step2 = new Deferred().done(function() { return step1 });
var step3 = new Deferred().done(function() { return step2 });

step1.done(function() { alert("Step 1") });
step2.done(function() { alert("Step 2") });
step3.done(function() { alert("All done") });
//now the 3 alerts will also be fired in order of 1,2,3
//no matter which Deferred gets resolved first.

step2.resolve();
step3.resolve();
step1.resolve();

2) Usalo per verificare lo stato dell'app:

var loggedIn = logUserInNow(); //deferred
var databaseReady = openDatabaseNow(); //deferred

jQuery.when(loggedIn, databaseReady).then(function() {
  //do something
});

2

È possibile utilizzare un oggetto differito per creare un design fluido che funzioni bene nei browser webkit. I browser Webkit genereranno un evento di ridimensionamento per ogni pixel in cui la finestra viene ridimensionata, a differenza di FF e IE che attivano l'evento solo una volta per ogni ridimensionamento. Di conseguenza, non si ha alcun controllo sull'ordine in cui verranno eseguite le funzioni associate all'evento di ridimensionamento della finestra. Qualcosa del genere risolve il problema:

var resizeQueue = new $.Deferred(); //new is optional but it sure is descriptive
resizeQueue.resolve();

function resizeAlgorithm() {
//some resize code here
}

$(window).resize(function() {
    resizeQueue.done(resizeAlgorithm);
});

Questo serializzerà l'esecuzione del tuo codice in modo che venga eseguito come previsto. Prestare attenzione alle insidie ​​quando si passano i metodi oggetto come callback a un rinviato. Una volta eseguito tale metodo come callback per rinviato, il riferimento 'this' verrà sovrascritto con riferimento all'oggetto differito e non farà più riferimento all'oggetto a cui appartiene il metodo.


Come fa questa serializzazione? Hai già risolto la coda, quindi resizeQueue.done(resizeAlgorithm)è esattamente lo stesso di resizeAlgorithm. È una finzione completa!
Raynos,

Quando il codice dell'algoritmo di ridimensionamento è complesso, l'implementazione JavaScript nel webkit perderà la sincronizzazione quando viene chiamata la funzione per ciascun pixel che ridimensiona la finestra. Differito mantiene i callback in una coda e li esegue in un ordine FIFO. Pertanto, se si aggiunge un callback "completato" e viene eseguito immediatamente perché il differito è già risolto, un altro callback "completato" che viene aggiunto al differito mentre il primo callback è ancora in esecuzione verrà aggiunto alla coda e dovrà attendere per restituire il primo callback. Spero che questo risponda alla tua domanda.
Miloš Rašić

l'interprete JS nel browser è a thread singolo. A meno che l'algoritmo di ridimensionamento non contenga un codice asincrono al suo interno, l'intera funzione dovrebbe aver finito di funzionare prima che .donevenga effettuata la chiamata successiva .
Raynos

@Raynos: ne sono consapevole, ma ho provato a chiamare semplicemente l'algoritmo resize su ridimensionamento e dà una pagina bianca vuota nei browser webkit mentre funzionava perfettamente in altri. Il differito risolve questo problema. Non ho avuto abbastanza tempo per fare qualche ricerca più approfondita in questo. Potrebbe essere un bug di webkit. Non credo che il differito usato nel mio esempio sarebbe di aiuto se resizeAlgorithm avesse del codice asincrono.
Miloš Rašić il

2
Non dovresti usare qualcosa come il plugin throttle / debounce benalman.com/projects/jquery-throttle-debounce-plugin per impedire alle tue funzioni di sparare più tahn una volta per ridimensionamento.
whereesrhys

2

Puoi anche integrarlo con qualsiasi libreria di terze parti che utilizza JQuery.

Una di queste librerie è Backbone, che in realtà supporterà Deferred nella prossima versione.


2
Utilizzare read more hereal posto di on my blog. È una pratica migliore e può salvarti la risposta da (accidentalmente) spam. :)
Lokesh Mehra

1

Ho appena usato Deferred in codice reale. Nel progetto jQuery Terminal ho la funzione exec che chiama i comandi definiti dall'utente (come se lo stesse inserendo e premendo invio), ho aggiunto Deferreds all'API e ho chiamato exec con array. come questo:

terminal.exec('command').then(function() {
   terminal.echo('command finished');
});

o

terminal.exec(['command 1', 'command 2', 'command 3']).then(function() {
   terminal.echo('all commands finished');
});

i comandi possono eseguire codice asincrono ed exec deve chiamare il codice utente in ordine. Il mio primo API utilizza una coppia di chiamate di pausa / ripresa e nella nuova API le chiamo automatiche quando l'utente promette di tornare. Quindi il codice utente può semplicemente usare

return $.get('/some/url');

o

var d = new $.Deferred();
setTimeout(function() {
    d.resolve("Hello Deferred"); // resolve value will be echoed
}, 500);
return d.promise();

Uso il codice in questo modo:

exec: function(command, silent, deferred) {
    var d;
    if ($.isArray(command)) {
        return $.when.apply($, $.map(command, function(command) {
            return self.exec(command, silent);
        }));
    }
    // both commands executed here (resume will call Term::exec)
    if (paused) {
        // delay command multiple time
        d = deferred || new $.Deferred();
        dalyed_commands.push([command, silent, d]);
        return d.promise();
    } else {
        // commands may return promise from user code
        // it will resolve exec promise when user promise
        // is resolved
        var ret = commands(command, silent, true, deferred);
        if (!ret) {
            if (deferred) {
                deferred.resolve(self);
                return deferred.promise();
            } else {
                d = new $.Deferred();
                ret = d.promise();
                ret.resolve();
            }
        }
        return ret;
    }
},

dalyed_commands viene utilizzato nella funzione resume che chiama di nuovo exec con tutti i dalyed_commands.

e parte della funzione comandi (ho eliminato parti non correlate)

function commands(command, silent, exec, deferred) {

    var position = lines.length-1;
    // Call user interpreter function
    var result = interpreter.interpreter(command, self);
    // user code can return a promise
    if (result != undefined) {
        // new API - auto pause/resume when using promises
        self.pause();
        return $.when(result).then(function(result) {
            // don't echo result if user echo something
            if (result && position === lines.length-1) {
                display_object(result);
            }
            // resolve promise from exec. This will fire
            // code if used terminal::exec('command').then
            if (deferred) {
                deferred.resolve();
            }
            self.resume();
        });
    }
    // this is old API
    // if command call pause - wait until resume
    if (paused) {
        self.bind('resume.command', function() {
            // exec with resume/pause in user code
            if (deferred) {
                deferred.resolve();
            }
            self.unbind('resume.command');
        });
    } else {
        // this should not happen
        if (deferred) {
            deferred.resolve();
        }
    }
}

1

La risposta di ehynds non funzionerà, perché memorizza nella cache i dati delle risposte. Dovrebbe memorizzare nella cache jqXHR che è anche una promessa. Ecco il codice corretto:

var cache = {};

function getData( val ){

    // return either the cached value or an
    // jqXHR object (which contains a promise)
    return cache[ val ] || $.ajax('/foo/', {
        data: { value: val },
        dataType: 'json',
        success: function(data, textStatus, jqXHR){
            cache[ val ] = jqXHR;
        }
    });
}

getData('foo').then(function(resp){
    // do something with the response, which may
    // or may not have been retreived using an
    // XHR request.
});

La risposta di Julian D. funzionerà correttamente ed è una soluzione migliore.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.