Wie kann jQuery Deferred verwendet werden?

279

jQuery 1.5 bringt das neue Rechnungsabgrenzungs Objekt und die beigefügten Methoden .when, .Deferredund ._Deferred.

Für diejenigen, die noch nicht verwendet .Deferredhaben, habe ich die Quelle dafür mit Anmerkungen versehen .

Was sind die möglichen Verwendungsmöglichkeiten dieser neuen Methoden, wie passen wir sie in Muster an?

Ich habe die API und die Quelle bereits gelesen und weiß, was sie bewirkt. Meine Frage ist, wie wir diese neuen Funktionen im alltäglichen Code verwenden können.

Ich habe ein einfaches Beispiel für eine Pufferklasse, die die AJAX-Anforderung der Reihe nach aufruft. (Der nächste Start nach dem vorherigen endet).

/* 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();
    };
};

Ich suche nach Demonstrationen und Verwendungsmöglichkeiten von .Deferredund .when.

Es wäre auch schön, Beispiele zu sehen ._Deferred.

Das Verknüpfen mit der neuen jQuery.ajaxQuelle für Beispiele ist Betrug.

Mich interessiert besonders, welche Techniken verfügbar sind, wenn wir abstrahieren, ob eine Operation synchron oder asynchron ausgeführt wird.

Raynos
quelle
19
Aus den FAQ: Vermeiden Sie es , subjektive Fragen zu stellen, bei denen ... jede Antwort gleichermaßen gültig ist: "Was ist Ihr Favorit ______?" (ihre Betonung)
TJ Crowder
2
@TJCrowser Ich werde versuchen, es neu zu formulieren.
Raynos
5
Es ist eine gute Frage , aber es kann nicht sein , dass viele Menschen , die beantworten :-)
Zipfel
2
@Pointy Ich schaue hauptsächlich auf diejenigen, die es verwendet haben, als es ein Plugin eines Drittanbieters war. Und die Leute ermutigen, sich zu setzen und es zu benutzen!
Raynos
1
._Deferredist einfach das wahre "Zurückgestellte Objekt", das .Deferredverwendet. Es ist ein internes Objekt, das Sie höchstwahrscheinlich nie brauchen werden.
David Tang

Antworten:

212

Der beste Anwendungsfall, den ich mir vorstellen kann, ist das Zwischenspeichern von AJAX-Antworten. Hier ist ein modifiziertes Beispiel aus Rebecca Murpheys Intro-Post zum Thema :

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

Grundsätzlich gilt, wenn der Wert bereits einmal angefordert wurde, bevor er sofort aus dem Cache zurückgegeben wird. Andernfalls ruft eine AJAX-Anforderung die Daten ab und fügt sie dem Cache hinzu. Das $.when/ .thenkümmert sich nicht darum; Alles, worüber Sie sich Sorgen machen müssen, ist die Verwendung der Antwort, die .then()in beiden Fällen an den Handler übergeben wird. jQuery.when()behandelt ein Nicht-Versprechen / Zurückgestellt als Abgeschlossen und führt sofort eines .done()oder .then()in der Kette aus.

Zurückgestellte sind perfekt, wenn die Aufgabe asynchron ausgeführt werden kann oder nicht und Sie diese Bedingung aus dem Code abstrahieren möchten.

Ein weiteres Beispiel aus der $.whenPraxis mit dem Helfer:

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

});
ehynds
quelle
4
Zwei brillante Beispiele. Ich habe etwas Ähnliches wie das zweite implementiert, aber mit 4 Ajax-Anforderungen, und es funktioniert gut und ist außerdem weitaus lesbarer, kompakter, logischer, wartbarer usw. jQuery.Deferred ist eine wirklich gute Sache.
PJP
5
Hier ist ein nützliches Video zu diesem Thema bigbinary.com/videos/3-using-deferred-in-jquery
Nick Vanderbilt
5
Das Caching funktioniert nicht, wenn das Ergebnis ein falscher Wert ist. Außerdem gefällt mir nicht, dass getData je nach Zweig zwei verschiedene Typen zurückgibt.
Marko Dumic
3
In der Antwort von Julian D. unten finden Sie eine bessere Implementierung des Ajax-Caching.
event_jr
1
Ich verstehe nicht, wie das erste Codebeispiel überhaupt funktioniert: Ich verstehe den Fall, in dem das Objekt nicht zwischengespeichert wird, aber wenn es dann nicht cache[ val ]ein Versprechen zurückgibt (die jquery-Dokumentation besagt, dass der Parameter die vom Absender zurückgegebenen Daten sind), was dies bedeutet Der Mitgliederzugriff von .thenWill Fehler ... richtig? Was vermisse ich?
Chacham15
79

Hier ist eine etwas andere Implementierung eines AJAX-Cache als in der Antwort von ehynd .

Wie in der Folgefrage von FortuneRice erwähnt, hat die Implementierung von ehynd nicht tatsächlich mehrere identische Anforderungen verhindert, wenn die Anforderungen ausgeführt wurden, bevor eine von ihnen zurückgekehrt war. Das ist,

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

wird höchstwahrscheinlich zu 3 AJAX-Anforderungen führen, wenn das Ergebnis für "xxx" noch nicht zuvor zwischengespeichert wurde.

Dies kann gelöst werden, indem anstelle des Ergebnisses die verzögerten Anforderungen der Anforderung zwischengespeichert werden:

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.
});
Julian D.
quelle
1
Ich denke, das ist immer noch nicht perfekt, da Sie den Cache beim ersten Abrufen nie leeren / aktualisieren. Dadurch funktioniert der AJAX-Aufruf bei keinem Update.
Zyzyis
45

Ein Aufgeschobener kann anstelle eines Mutex verwendet werden. Dies entspricht im Wesentlichen den Szenarien für die Verwendung mehrerer Ajax.

MUTEX

var mutex = 2;

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

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

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

VERZÖGERT

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
});

Achten Sie bei der Verwendung eines Deferred nur als Mutex auf Leistungseinbußen (http://jsperf.com/deferred-vs-mutex/2). Obwohl sich die Bequemlichkeit sowie die zusätzlichen Vorteile eines verzögerten Einsatzes durchaus lohnen und bei der tatsächlichen (benutzergesteuerten ereignisbasierten) Nutzung die Auswirkungen auf die Leistung nicht spürbar sein sollten.

user406905
quelle
Es war überraschend schwierig für mich, dies zu finden. Ich habe es für eine Funktion verwendet, die ein setInterval enthält, das das aufgelöste Versprechen zurückgibt und sich selbst zerstört, sobald die Breite von div eine bestimmte Zahl überschreitet. Es war zur Fehlerbehebung und eine Lösung, wenn ich mein Problem nicht lösen konnte, aber ich bin begeistert davon.
JSG
28

Dies ist eine Antwort für sich selbst, aber ich habe einige Monate damit verbracht, dies zu untersuchen und die Ergebnisse auf der jQuery-Konferenz in San Francisco 2012 vorzustellen.

Hier ist ein kostenloses Video des Vortrags:

https://www.youtube.com/watch?v=juRtEEsHI9E

Alex Mcp
quelle
20

Eine andere Verwendung, die ich sinnvoll eingesetzt habe, ist das Abrufen von Daten aus mehreren Quellen. Im folgenden Beispiel rufe ich mehrere unabhängige JSON-Schemaobjekte ab, die in einer vorhandenen Anwendung zur Validierung zwischen einem Client und einem REST-Server verwendet werden. In diesem Fall möchte ich nicht, dass die browserbasierte Anwendung mit dem Laden von Daten beginnt, bevor alle Schemas geladen sind. $ .when.apply (). then () ist dafür perfekt. Vielen Dank an Raynos für Hinweise zur Verwendung von then (fn1, fn2) zur Überwachung auf Fehlerzustände.

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);
});     
Elf Sternberg
quelle
10

Ein weiteres Beispiel Deferredfür die Implementierung eines Caches mit s für jede Art von Berechnung (normalerweise einige leistungsintensive oder lang laufende Aufgaben):

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("|");
};

Hier ist ein Beispiel für die Verwendung dieser Klasse, um eine (simulierte schwere) Berechnung durchzuführen:

// 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);
});

Der gleiche zugrunde liegende Cache kann zum Zwischenspeichern von Ajax-Anforderungen verwendet werden:

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);
});

Sie können mit dem obigen Code in dieser jsFiddle spielen .

Julian D.
quelle
9

1) Verwenden Sie diese Option, um eine geordnete Ausführung von Rückrufen sicherzustellen:

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) Verwenden Sie diese Option, um den Status der App zu überprüfen:

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

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

Sie können ein zurückgestelltes Objekt verwenden, um ein flüssiges Design zu erstellen, das in Webkit-Browsern gut funktioniert. Webkit-Browser lösen ein Größenänderungsereignis für jedes Pixel aus, dessen Fenstergröße geändert wird, im Gegensatz zu FF und IE, die das Ereignis nur einmal für jede Größenänderung auslösen. Infolgedessen haben Sie keine Kontrolle über die Reihenfolge, in der die an Ihr Fenstergrößenänderungsereignis gebundenen Funktionen ausgeführt werden. So etwas löst das Problem:

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);
});

Dadurch wird die Ausführung Ihres Codes so serialisiert, dass er wie beabsichtigt ausgeführt wird. Achten Sie auf Fallstricke, wenn Sie Objektmethoden als Rückruf an einen Zurückgestellten übergeben. Sobald eine solche Methode als Rückruf auf verzögert ausgeführt wird, wird die 'this'-Referenz mit Bezug auf das zurückgestellte Objekt überschrieben und bezieht sich nicht mehr auf das Objekt, zu dem die Methode gehört.

Miloš Rašić
quelle
Wie macht das eine Serialisierung? Sie haben die Warteschlange bereits aufgelöst und resizeQueue.done(resizeAlgorithm)sind genau so wie resizeAlgorithm. Es ist eine völlige Täuschung!
Raynos
Wenn der Code Ihres resizeAlgorithm komplex ist, verliert die JavaScript-Implementierung im Webkit die Synchronisation, wenn die Funktion für jedes Pixel aufgerufen wird, dessen Größe Sie im Fenster ändern. Aufgeschoben hält Ihre Rückrufe in einer Warteschlange und führt sie in einer FIFO-Reihenfolge aus. Wenn Sie also einen Rückruf "erledigt" hinzufügen und dieser sofort ausgeführt wird, weil der verzögerte Rückruf bereits behoben ist, wird ein weiterer "erledigter" Rückruf, der dem zurückgestellten hinzugefügt wird, während der erste Rückruf noch ausgeführt wird, zur Warteschlange hinzugefügt und muss warten für den ersten Rückruf zurück. Ich hoffe das beantwortet deine Frage.
Miloš Rašić
Der JS-Interpreter im Browser ist Single-Threaded. Sofern Ihr resizeAlgorithm keinen asynchronen Code enthält, sollte die gesamte Funktion vor dem nächsten Aufruf von beendet .donesein.
Raynos
@ Raynos: Ich bin mir dessen bewusst, aber ich habe versucht, den resizeAlgorithm beim Ändern der Größe einfach aufzurufen. In Webkit-Browsern wird eine leere weiße Seite angezeigt, während in anderen Browsern einwandfrei funktioniert wird. Der Aufgeschobene löst dieses Problem. Ich hatte nicht genug Zeit, um dies genauer zu untersuchen. Könnte ein Webkit-Fehler sein. Ich glaube nicht, dass die in meinem Beispiel verwendete Verzögerung helfen würde, wenn resizeAlgorithm asynchronen Code hätte.
Miloš Rašić
2
Sollten Sie nicht so etwas wie das Gas- / Entprellungs-Plugin benalman.com/projects/jquery-throttle-debounce-plugin verwenden , um zu verhindern, dass Ihre Funktionen einmal pro Größenänderung mehr Tahn auslösen.
Wheresrhys
2

Sie können es auch in Bibliotheken von Drittanbietern integrieren, die JQuery verwenden.

Eine solche Bibliothek ist Backbone, die Deferred in ihrer nächsten Version tatsächlich unterstützen wird.

Diego
quelle
2
Verwenden Sie read more hereanstelle von on my blog. Dies ist eine bessere Vorgehensweise und kann Ihnen die Antwort vor (versehentlich) Spam ersparen. :)
Lokesh Mehra
1

Ich habe Deferred gerade in echtem Code verwendet. Im Projekt jQuery Terminal habe ich die Funktion exec, die vom Benutzer definierte Befehle aufruft (wie er sie eingegeben und die Eingabetaste gedrückt hat). Ich habe der API Zurückgestellte hinzugefügt und exec mit Arrays aufgerufen. so was:

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

oder

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

Die Befehle können asynchronen Code ausführen, und exec muss den Benutzercode der Reihe nach aufrufen. Meine erste API verwendet zwei Pausen- / Wiederaufnahme-Aufrufe und in der neuen API rufe ich diese automatisch auf, wenn der Benutzer das Versprechen zurückgibt. Benutzercode kann also einfach verwendet werden

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

oder

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

Ich benutze folgenden Code:

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 wird in der Resume-Funktion verwendet, die exec mit allen dalyed_commands erneut aufruft.

und ein Teil der Befehlsfunktion (ich habe nicht verwandte Teile entfernt)

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();
        }
    }
}
jcubic
quelle
1

Die Antwort von ehynds funktioniert nicht, da die Antwortdaten zwischengespeichert werden. Es sollte das jqXHR zwischenspeichern, was auch ein Versprechen ist. Hier ist der richtige Code:

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

Die Antwort von Julian D. wird korrekt funktionieren und ist eine bessere Lösung.

John Berg
quelle