¿Cómo se puede usar jQuery diferido?

279

jQuery 1.5 trae el nuevo objeto diferido y los métodos adjuntos .when, .Deferredy ._Deferred.

Para aquellos que no han usado .Deferredantes, he anotado la fuente para ello .

¿Cuáles son los posibles usos de estos nuevos métodos, cómo hacemos para adaptarlos a los patrones?

Ya he leído la API y la fuente , así que sé lo que hace. Mi pregunta es ¿cómo podemos usar estas nuevas funciones en el código cotidiano?

Tengo un ejemplo simple de una clase de búfer que llama a la solicitud AJAX en orden. (El siguiente comienza después del anterior).

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

Estoy buscando demostraciones y posibles usos de .Deferredy .when.

También sería encantador ver ejemplos de ._Deferred.

Vinculación a lo nuevo jQuery.ajax fuente de ejemplos es hacer trampa.

Estoy particularmente interesado en las técnicas disponibles cuando abstraemos si una operación se realiza de forma sincrónica o asincrónica.

Raynos
fuente
19
De las preguntas frecuentes: evite hacer preguntas subjetivas donde ... cada respuesta es igualmente válida: "¿Cuál es tu ______ favorito?" (su énfasis)
TJ Crowder
2
@TJCrowser Voy a ver cómo volver a redactarlo.
Raynos
55
Es una buena pregunta, pero no puede ser que muchas personas que pueden contestar :-)
puntiagudo
2
@Pointy Principalmente veo a aquellos que lo usaron cuando era un complemento de terceros. ¡Y alentar a las personas a sentarse y usarlo!
Raynos
1
._Deferredes simplemente el verdadero "objeto diferido" que .Deferredutiliza. Es un objeto interno que probablemente nunca necesitarás.
David Tang

Respuestas:

212

El mejor caso de uso que se me ocurre es el almacenamiento en caché de las respuestas AJAX. Aquí hay un ejemplo modificado de la publicación de introducción de Rebecca Murphey sobre el tema :

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

Básicamente, si el valor ya se solicitó una vez antes, se devuelve inmediatamente desde la memoria caché. De lo contrario, una solicitud AJAX obtiene los datos y los agrega a la memoria caché. Al $.when/ .thenno le importa nada de esto; todo lo que debe preocuparle es usar la respuesta, que se pasa al .then()controlador en ambos casos. jQuery.when()maneja un No Prometido / Diferido como Completado, ejecutando inmediatamente cualquiera .done()o .then()en la cadena.

Los diferidos son perfectos para cuando la tarea puede o no funcionar de forma asincrónica, y desea abstraer esa condición del código.

Otro ejemplo del mundo real usando el $.whenayudante:

$.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
fuente
44
Dos ejemplos brillantes. Implementé algo similar al segundo, pero con 4 solicitudes ajax, y funciona bien, además de ser mucho más legible, compacto, lógico, mantenible, etc. jQuery.Perferido es algo realmente bueno.
PJP
55
Aquí hay un video útil sobre este tema bigbinary.com/videos/3-using-deferred-in-jquery
Nick Vanderbilt
55
El almacenamiento en caché no funcionará si el resultado es un valor falso. Además, no me gusta el hecho de que getData devuelve 2 tipos diferentes dependiendo de la rama tomada.
Marko Dumic
3
Vea la respuesta de Julian D. a continuación para una mejor implementación del almacenamiento en caché de ajax.
event_jr
1
No entiendo cómo funciona el primer ejemplo de código: entiendo el caso en el que el objeto no se almacena en caché, pero si no se cache[ val ]devuelve una promesa (la documentación de jquery dice que el parámetro son los datos devueltos por el remitente) lo que significa que el acceso de miembros de .thenwill error ... ¿verdad? ¿Qué me estoy perdiendo?
chacham15
79

Aquí hay una implementación ligeramente diferente de un caché AJAX como en la respuesta de ehynd .

Como se señaló en la pregunta de seguimiento de fortuneRice, la implementación de ehynd en realidad no evitó múltiples solicitudes idénticas si las solicitudes se realizaron antes de que uno de ellos regresara. Es decir,

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

probablemente dará como resultado 3 solicitudes AJAX si el resultado de "xxx" no se ha almacenado en caché antes.

Esto se puede resolver almacenando en caché los aplazamientos de la solicitud en lugar del resultado:

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.
fuente
1
Creo que esto todavía no es perfecto, ya que nunca se borra / actualiza el caché una vez que se obtiene por primera vez. Esto hará que la llamada AJAX no funcione para ninguna actualización.
zyzyis
45

Se puede usar un diferido en lugar de un mutex. Esto es esencialmente lo mismo que los múltiples escenarios de uso de ajax.

MUTEX

var mutex = 2;

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

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

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

DIFERIDO

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

Cuando utilice un diferido solo como mutex, tenga cuidado con los impactos en el rendimiento (http://jsperf.com/deferred-vs-mutex/2). Aunque la conveniencia, así como los beneficios adicionales proporcionados por un diferido bien valen la pena, y en el uso real (basado en eventos impulsados ​​por el usuario), el impacto en el rendimiento no debería ser notable.

usuario406905
fuente
Fue sorprendentemente difícil para mí encontrar esto. Lo utilicé en una función que contiene un setInterval que devolvería la promesa resuelta y se autodestruirá una vez que el ancho de div supere un cierto número. Fue para la solución de problemas y una solución si no podía resolver mi problema, pero estoy muy emocionado al respecto.
JSG
28

Esta es una respuesta autopromocional, pero pasé unos meses investigando esto y presenté los resultados en jQuery Conference San Francisco 2012.

Aquí hay un video gratuito de la charla:

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

Alex Mcp
fuente
20

Otro uso que he estado haciendo con un buen propósito es obtener datos de múltiples fuentes. En el ejemplo a continuación, busco varios objetos de esquema JSON independientes utilizados en una aplicación existente para la validación entre un cliente y un servidor REST. En este caso, no quiero que la aplicación del lado del navegador comience a cargar datos antes de que se hayan cargado todos los esquemas. $ .when.apply (). then () es perfecto para esto. Gracias a Raynos por los indicadores sobre el uso de entonces (fn1, fn2) para monitorear las condiciones de error.

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
fuente
10

Otro ejemplo que usa Deferreds para implementar un caché para cualquier tipo de cómputo (generalmente algunas tareas de alto rendimiento o de ejecución prolongada):

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

Aquí hay un ejemplo del uso de esta clase para realizar algunos cálculos (pesados ​​simulados):

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

El mismo caché subyacente podría usarse para almacenar en caché las solicitudes de 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);
});

Puedes jugar con el código anterior en este jsFiddle .

Julian D.
fuente
9

1) Úselo para garantizar una ejecución ordenada de devoluciones de llamada:

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) Úselo para verificar el estado de la aplicación:

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

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

Puede usar un objeto diferido para hacer un diseño fluido que funcione bien en los navegadores webkit. Los navegadores de Webkit activarán el evento de cambio de tamaño para cada píxel de la ventana, a diferencia de FF e IE, que activan el evento solo una vez para cada cambio de tamaño. Como resultado, no tiene control sobre el orden en que se ejecutarán las funciones vinculadas a su evento de cambio de tamaño de ventana. Algo como esto resuelve el 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);
});

Esto serializará la ejecución de su código para que se ejecute como lo desea. Tenga cuidado con las trampas al pasar métodos de objeto como devoluciones de llamada a un diferido. Una vez que dicho método se ejecuta como una devolución de llamada diferida, la referencia 'this' se sobrescribirá con referencia al objeto diferido y ya no se referirá al objeto al que pertenece el método.

Miloš Rašić
fuente
¿Cómo hace esto alguna serialización? Ya ha resuelto la cola, por lo que resizeQueue.done(resizeAlgorithm)es exactamente lo mismo que resizeAlgorithm. Es una farsa completa!
Raynos
Cuando el código de su resizeAlgorithm es complejo, la implementación de JavaScript en webkit perderá la sincronización cuando se llame a la función para cada píxel al que redimensione la ventana. Diferido mantiene sus devoluciones de llamada en una cola y las ejecuta en un orden FIFO. Por lo tanto, si agrega una devolución de llamada 'hecha' y se ejecuta inmediatamente porque el aplazado ya está resuelto, otra devolución de llamada 'terminada' que se agrega al aplazado mientras la primera devolución de llamada aún se está ejecutando se agregará a la cola y tendrá que esperar para que vuelva la primera devolución de llamada. Espero que esto responda tu pregunta.
Miloš Rašić
El intérprete JS en el navegador es de un solo subproceso. A menos que resizeAlgorithm tenga algún código asíncrono, toda la función debería haber terminado de funcionar antes de realizar la siguiente llamada .done.
Raynos
@Raynos: Soy consciente de eso, pero intenté simplemente llamar al resizeAlgorithm on resize y da una página en blanco en los navegadores webkit mientras funcionaba perfectamente en otros. El diferido resuelve este problema. No he tenido tiempo suficiente para hacer una investigación más profunda sobre esto. Podría ser un error de webkit. No creo que el diferido como se usa en mi ejemplo ayudaría si resizeAlgorithm tuviera algún código asincrónico.
Miloš Rašić
2
¿No deberías estar usando algo como el plugin de aceleración / rebote benalman.com/projects/jquery-throttle-debounce-plugin para evitar que tus funciones se activen más de una vez por cambio de tamaño?
wheresrhys
2

También puede integrarlo con cualquier biblioteca de terceros que utilice JQuery.

Una de esas bibliotecas es Backbone, que en realidad admitirá Diferido en su próxima versión.

Diego
fuente
2
Uso read more hereen lugar de on my blog. Es una mejor práctica y puede salvar su respuesta de (accidentalmente) ser spam. :)
Lokesh Mehra
1

Acabo de usar diferido en código real. En el proyecto jQuery Terminal, tengo una función ejecutiva que llama a comandos definidos por el usuario (como si la estuviera ingresando y presionando enter), agregué aplazamientos a la API y llamé a la ejecutiva con matrices. Me gusta esto:

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

los comandos pueden ejecutar código asíncrono, y el ejecutivo necesita llamar al código de usuario en orden. Mi primera API utiliza un par de llamadas de pausa / reanudar y en una nueva API las llamo automáticamente cuando el usuario promete. Entonces el código de usuario solo puede usar

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

o

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

Yo uso un código como este:

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 se utiliza en la función de reanudación que llama a exec nuevamente con todos los dalyed_commands.

y parte de la función de comandos (he eliminado partes no relacionadas)

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
fuente
1

La respuesta de ehynds no funcionará porque almacena en caché los datos de las respuestas. Debe almacenar en caché el jqXHR, que también es una promesa. Aquí está el código correcto:

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 respuesta de Julian D. funcionará correctamente y es una mejor solución.

John Berg
fuente