Comment utiliser jQuery différé?


279

jQuery 1.5 apporte le nouvel objet Deferred et les méthodes associées .when, .Deferredet ._Deferred.

Pour ceux qui ne l'ont pas utilisé .Deferredauparavant, j'ai annoté la source de celui-ci .

Quels sont les usages possibles de ces nouvelles méthodes, comment procéder pour les adapter à des motifs?

J'ai déjà lu l' API et la source , donc je sais ce que ça fait. Ma question est de savoir comment utiliser ces nouvelles fonctionnalités dans le code de tous les jours?

J'ai un exemple simple d'une classe tampon qui appelle la demande AJAX dans l'ordre. (Début suivant après fin du 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 .Deferredet .when.

Il serait également agréable d'en voir des exemples ._Deferred.

Lier à la nouvelle jQuery.ajaxsource d'exemples est de la triche.

Je suis particulièrement intéressé par les techniques disponibles lorsque nous résumons si une opération est effectuée de manière synchrone ou asynchrone.


19
De la FAQ: évitez de poser des questions subjectives où ... chaque réponse est également valable: "Quel est votre ______ préféré?" (leur emphase)
TJ Crowder

2
@TJCrowser Je vais essayer de le reformuler.
Raynos

5
Il est une bonne question , mais il ne peut pas être que beaucoup de gens qui peuvent répondre :-)
Pointy

2
@Pointy Je regarde principalement ceux qui l'ont utilisé quand c'était un plugin tiers. Et encourager les gens à s'asseoir et à l'utiliser!
Raynos

1
._Deferredest tout simplement le véritable "objet différé" qui .Deferredutilise. C'est un objet interne dont vous n'aurez probablement jamais besoin.
David Tang

Réponses:


212

Le meilleur cas d'utilisation auquel je puisse penser est la mise en cache des réponses AJAX. Voici un exemple modifié du post d'introduction 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 immédiatement renvoyée par le cache. Sinon, une demande AJAX récupère les données et les ajoute au cache. Le $.when/ .thenne se soucie de rien de tout cela; tout ce dont vous avez besoin, c'est d'utiliser la réponse, qui est transmise au .then()gestionnaire dans les deux cas. jQuery.when()gère un non-promis / différé comme un terminé, exécutant immédiatement tout .done()ou .then()sur la chaîne.

Les différés sont parfaits lorsque la tâche peut fonctionner de manière asynchrone ou non, et que vous souhaitez retirer cette condition du code.

Un autre exemple du monde réel utilisant l' $.whenaide:

$.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
Deux exemples brillants. J'ai implémenté quelque chose de similaire au 2ème, mais avec 4 requêtes ajax, et il fonctionne bien, en plus d'être beaucoup plus lisible, compact, logique, maintenable, etc. jQuery.Deferred est une très bonne chose.
PJP

5
Voici une vidéo utile sur ce sujet bigbinary.com/videos/3-using-deferred-in-jquery
Nick Vanderbilt

5
La mise en cache ne fonctionnera pas si le résultat est une valeur fausse. De plus, je n'aime pas le fait que getData renvoie 2 types différents selon la branche prise.
Marko Dumic

3
Voir la réponse de Julian D. ci-dessous pour une meilleure implémentation de la mise en cache ajax.
event_jr

1
Je ne comprends pas comment fonctionne le premier exemple de code: je comprends le cas où l'objet n'est pas mis en cache, mais s'il ne cache[ val ]retourne PAS de promesse (la documentation jquery dit que le paramètre est les données renvoyées par l'expéditeur), ce qui signifie que le membre accède à .thenl'erreur ... n'est-ce pas? Qu'est-ce que je rate?
chacham15

79

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 de suivi de fortuneRice, la mise en œuvre de ehynd n'a pas réellement empêché plusieurs demandes identiques si les demandes étaient effectuées avant le retour de l'une d'entre elles. C'est,

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

entraînera très probablement 3 requêtes AJAX si le résultat pour "xxx" n'a pas déjà été mis en cache auparavant.

Cela peut être résolu en mettant en cache les différés de la demande au lieu 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.
});

1
Je pense que ce n'est toujours pas parfait, car vous n'avez jamais effacé / mis à jour le cache une fois récupéré pour la première fois. Cela fera que l'appel AJAX ne fonctionnera pour aucune mise à jour.
zyzyis

45

Un différé peut être utilisé à la place d'un mutex. C'est essentiellement la même chose que les multiples scénarios d'utilisation 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 différé comme mutex uniquement, faites attention aux impacts 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 dans l'utilisation réelle (basée sur les événements utilisateur), l'impact sur les performances ne devrait pas être perceptible.


C'était étonnamment difficile pour moi de trouver cela. Je l'ai utilisé sur une fonction contenant un setInterval qui retournerait la promesse résolue et s'autodétruirait une fois que la largeur de div atteindrait un certain nombre. C'était pour le dépannage et une solution si je ne pouvais pas résoudre mon problème, mais je suis ravi.
JSG

28

Il s'agit d'une réponse auto-promotionnelle, mais j'ai passé quelques mois à faire des recherches à ce sujet et présenté les résultats lors de la conférence jQuery à San Francisco 2012.

Voici une vidéo gratuite de la conférence:

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


20

Une autre utilisation que j'ai mise à profit est la récupération de 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 la validation entre un client et un serveur REST. Dans ce cas, je ne veux pas que l'application côté navigateur commence à charger les données avant d'avoir chargé tous les schémas. $ .when.apply (). then () est parfait pour cela. Merci à Raynos pour les pointeurs sur l'utilisation de puis (fn1, fn2) pour surveiller les 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);
});     

10

Un autre exemple utilisant Deferreds pour implémenter un cache pour tout type de calcul (généralement certaines tâches exigeantes en performances ou de longue durée):

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 ce jsFiddle .


9

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 l'état de l'application:

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

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

2

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 l'événement de redimensionnement pour chaque pixel de la fenêtre est redimensionnée, contrairement à FF et IE qui ne déclenchent l'événement qu'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 s'exécuteront. 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 différé. Une fois que cette méthode est exécutée en tant que rappel de différé, la référence «cette» sera remplacée par référence à l'objet différé et ne fera plus référence à l'objet auquel la méthode appartient.


Comment cela fait-il une sérialisation? Vous avez déjà résolu la file d'attente, resizeQueue.done(resizeAlgorithm)c'est exactement la même chose que resizeAlgorithm. C'est une imposture complète!
Raynos

Lorsque le code de votre resizeAlgorithm est complexe, l'implémentation JavaScript dans le kit Web perdra la synchronisation lorsque la fonction est appelée pour chaque pixel que vous redimensionnez la fenêtre. Deferred conserve vos rappels dans une file d'attente et les exécute dans un ordre FIFO. Donc, si vous ajoutez un rappel «terminé» et qu'il s'exécute immédiatement car le différé est déjà résolu, un autre rappel «terminé» qui est ajouté au différé pendant que le premier rappel est toujours en cours d'exécution sera ajouté à la file d'attente et devra attendre pour le premier rappel à revenir. J'espère que cela répond à votre question.
Miloš Rašić

l'interpréteur JS dans le navigateur est un thread unique. À moins que votre resizeAlgorithm ne contienne du code asynchrone, la fonction entière devrait avoir fini de fonctionner avant le prochain appel à .done.
Raynos

@Raynos: J'en suis conscient, mais j'ai essayé d'appeler simplement resizeAlgorithm lors du redimensionnement et cela donne une page blanche vierge dans les navigateurs webkit tout en fonctionnant parfaitement dans les autres. Le différé résout ce problème. Je n'ai pas eu assez de temps pour faire des recherches plus approfondies à ce sujet. Peut-être un bug de webkit. Je ne pense pas que le report comme utilisé dans mon exemple aiderait si resizeAlgorithm avait du code asynchrone.
Miloš Rašić

2
Ne devriez-vous pas utiliser quelque chose comme le plugin throttle / debounce benalman.com/projects/jquery-throttle-debounce-plugin pour empêcher vos fonctions de tirer plus de tahn une fois par redimensionnement.

2

Vous pouvez également l'intégrer à toutes les bibliothèques tierces qui utilisent JQuery.

L'une de ces bibliothèques est Backbone, qui va réellement prendre en charge Deferred dans leur prochaine version.


2
Utiliser read more hereà la place de on my blog. C'est une meilleure pratique et peut vous éviter de répondre (accidentellement) au spam. :)
Lokesh Mehra

1

Je viens d'utiliser Deferred en vrai code. Dans le projet jQuery Terminal, j'ai une fonction exec qui appelle des commandes définies par l'utilisateur (comme s'il le saisissait et en appuyant sur Entrée), j'ai ajouté des différés à l'API et appelle 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 l'exec doit appeler le code utilisateur dans l'ordre. Ma première API utilise une paire d'appels de pause / reprise et dans la nouvelle API, j'appelle ces appels automatiques lorsque l'utilisateur retourne la promesse. Le code utilisateur peut donc 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 du 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 dalyed_commands.

et une partie de la fonction de commandes (j'ai supprimé 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();
        }
    }
}

1

La réponse de ehynds ne fonctionnera pas, car elle met en cache les données des réponses. Il devrait mettre en cache le jqXHR qui est également 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 est une meilleure solution.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.