jQuery 1.5 apporte le nouvel objet différé et les méthodes attachées .when
, .Deferred
et ._Deferred
.
Pour ceux qui n'ont pas utilisé _.Deferred
_ auparavant, j'ai annoté le source .
Quels sont les usages possibles de ces nouvelles méthodes, comment pouvons-nous les adapter à des modèles?
J'ai déjà lu le API et le source , je sais donc ce qu'il fait. Ma question est comment pouvons-nous utiliser ces nouvelles fonctionnalités dans le code quotidien?
J'ai un simple exemple d'une classe de mémoire tampon qui appelle AJAX demande dans l'ordre. (Le suivant commence après le précédent).
_/* 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();
};
};
_
Je recherche des démonstrations et des utilisations possibles de _.Deferred
_ et _.when
_.
Il serait également agréable de voir des exemples de _._Deferred
_.
La liaison à la nouvelle source jQuery.ajax
est un exemple.
Je suis particulièrement intéressé par les techniques disponibles lorsque nous déterminons si une opération est effectuée de manière synchrone ou asynchrone.
Le meilleur des cas d'utilisation auquel je puisse penser est la mise en cache des réponses AJAX. Voici un exemple modifié de post d'intro de Rebecca Murphey sur le sujet :
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.
});
Fondamentalement, si la valeur a déjà été demandée une fois avant d'être renvoyée immédiatement du cache. Sinon, une demande AJAX récupère les données et les ajoute au cache. Le $.when
/.then
ne se soucie de rien de tout cela. tout ce dont vous avez besoin est d'utiliser la réponse, qui est transmise au gestionnaire .then()
dans les deux cas. jQuery.when()
gère une promesse non différée/différée en tant que finalisée, en exécutant immédiatement toute .done()
ou .then()
de la chaîne.
Les différés sont parfaits lorsque la tâche peut ou non fonctionner de manière asynchrone et que vous souhaitez extraire cette condition du code.
Un autre exemple concret utilisant l’assistant $.when
:
$.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
});
Voici une implémentation légèrement différente d'un cache AJAX comme dans la réponse de ehynd .
Comme indiqué dans la question suivante de de fortuneRice , la mise en œuvre de ehynd n'a pas empêché plusieurs requêtes identiques si celles-ci avaient été exécutées avant le retour de l'une d'entre elles. C'est,
for (var i=0; i<3; i++) {
getData("xxx");
}
donnera probablement 3 AJAX requêtes si le résultat pour "xxx" n'a pas déjà été mis en cache auparavant.
Ceci peut être résolu en mettant en cache la demande différée à la place du résultat:
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.
});
Un différé peut être utilisé à la place d'un mutex. Ceci est essentiellement identique aux scénarios d’utilisation multiples ajax.
MUTEX
var mutex = 2;
setTimeout(function() {
callback();
}, 800);
setTimeout(function() {
callback();
}, 500);
function callback() {
if (--mutex === 0) {
//run code
}
}
DIFFÉRÉ
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
});
Lorsque vous utilisez un mode différé uniquement en tant que mutex, faites attention aux conséquences sur les performances (http://jsperf.com/deferred-vs-mutex/2). Bien que la commodité, ainsi que les avantages supplémentaires fournis par un différé, en valent la peine, et que, dans le cas d'une utilisation réelle (en fonction d'événements déterminés par l'utilisateur), l'impact sur les performances ne devrait pas être perceptible.
C’est une réponse auto-promotionnelle, mais j’ai passé quelques mois à effectuer des recherches et à présenter les résultats à la jQuery Conference San Francisco 2012.
Voici une vidéo gratuite de la conférence:
http://www.confreaks.com/videos/993-jqcon2012-i-promise-to-show-you-when-to-use-deferreds
Une autre utilisation que je mets à profit est de récupérer des données à partir de plusieurs sources. Dans l'exemple ci-dessous, je récupère plusieurs objets de schéma JSON indépendants utilisés dans une application existante pour les valider entre un client et un serveur REST. Dans ce cas, je ne souhaite pas que l'application côté navigateur commence à charger les données avant que tous les schémas ne soient chargés. $ .when.apply (). then () est parfait pour cela. Merci à Raynos pour les pointeurs sur l’utilisation ensuite (fn1, fn2) de la surveillance des conditions d’erreur.
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);
});
Un autre exemple utilisant Deferred
s pour implémenter un cache pour tout type de calcul (généralement des tâches à hautes performances ou longues):
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("|");
};
Voici un exemple d'utilisation de cette classe pour effectuer des calculs (lourds simulés):
// 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);
});
Le même cache sous-jacent pourrait être utilisé pour mettre en cache les requêtes 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);
});
Vous pouvez jouer avec le code ci-dessus dans this jsFiddle .
1) Utilisez-le pour assurer une exécution ordonnée des rappels:
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) Utilisez-le pour vérifier le statut de l'application:
var loggedIn = logUserInNow(); //deferred
var databaseReady = openDatabaseNow(); //deferred
jQuery.when(loggedIn, databaseReady).then(function() {
//do something
});
Vous pouvez utiliser un objet différé pour créer une conception fluide qui fonctionne bien dans les navigateurs Webkit. Les navigateurs Webkit déclenchent un événement de redimensionnement pour chaque pixel de la fenêtre, contrairement à FF et IE qui déclenchent l'événement une seule fois pour chaque redimensionnement. Par conséquent, vous n’avez aucun contrôle sur l’ordre dans lequel les fonctions liées à votre événement de redimensionnement de fenêtre seront exécutées. Quelque chose comme ça résout le problème:
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);
});
Cela sérialisera l'exécution de votre code afin qu'il s'exécute comme vous le souhaitiez. Méfiez-vous des pièges lors du passage de méthodes d'objet en tant que rappels à un objet différé. Une fois que cette méthode est exécutée en tant que rappel du différé, la référence "ceci" sera remplacée par la référence à l'objet différé et ne fera plus référence à l'objet auquel la méthode appartient.
Vous pouvez également l'intégrer à toutes les bibliothèques tierces utilisant JQuery.
Une de ces bibliothèques est Backbone, qui supportera en fait Deferred dans leur prochaine version. J'en ai parlé aussi sur mon blog
Je viens d'utiliser différé dans le code réel. Dans le projet jQuery Terminal J'ai la fonction exec qui appelle les commandes définies par l'utilisateur (comme s'il la saisissait et appuyait sur entrée), j'ai ajouté Deferreds à l'API et appeler exec avec des tableaux. comme ça:
terminal.exec('command').then(function() {
terminal.echo('command finished');
});
ou
terminal.exec(['command 1', 'command 2', 'command 3']).then(function() {
terminal.echo('all commands finished');
});
les commandes peuvent exécuter du code asynchrone, et exec doit appeler un code d'utilisateur dans l'ordre. Ma première API utilise une paire d’appels en pause/reprise et, dans la nouvelle API, j’appelle celles-ci automatiquement lorsque l’utilisateur retourne sa promesse. Donc, le code d'utilisateur peut simplement utiliser
return $.get('/some/url');
ou
var d = new $.Deferred();
setTimeout(function() {
d.resolve("Hello Deferred"); // resolve value will be echoed
}, 500);
return d.promise();
J'utilise un code comme celui-ci:
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 est utilisé dans la fonction de reprise qui appelle à nouveau exec avec toutes les commandes dalyed.
et une partie de la fonction des commandes (j'ai dépouillé les parties non liées)
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();
}
}
}
La réponse par ehynds ne fonctionnera pas, car elle met en cache les données de réponses. Il devrait mettre en cache le jqXHR qui est aussi une promesse. Voici le bon 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.
});
La réponse de Julian D. fonctionnera correctement et constitue une meilleure solution.