Como o jQuery diferido pode ser usado?


279

O jQuery 1.5 traz o novo objeto Adiado e os métodos anexados .when,.Deferred e ._Deferred.

Para aqueles que não usaram .Deferredantes, eu anotei o fonte .

Quais são os possíveis usos desses novos métodos, como vamos ajustá-los aos padrões?

Eu já li a API e a fonte , então sei o que ela faz. Minha pergunta é como podemos usar esses novos recursos no código cotidiano?

Eu tenho um exemplo simples de uma classe de buffer que chama a solicitação AJAX em ordem. (Começo seguinte depois que o anterior termina).

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

Estou à procura de demonstrações e possíveis usos de .Deferrede .when.

Também seria adorável ver exemplos de ._Deferred.

Vinculando ao novo jQuery.ajax fonte de exemplos é trapaça.

Estou particularmente interessado em quais técnicas estão disponíveis quando abstraímos se uma operação é feita de maneira síncrona ou assíncrona.


19
No FAQ: evite fazer perguntas subjetivas onde ... todas as respostas são igualmente válidas: "Qual é o seu favorito ______?" (ênfase)
TJ Crowder

2
@TJCrowser Vou olhar para reformulá-lo.
Raynos

5
É uma boa pergunta, mas não pode haver que muitas pessoas que podem responder :-)
Pointy

2
@ Pointy Eu estou olhando principalmente para aqueles que o usaram quando era um plug-in de terceiros. E incentivar as pessoas a se sentarem e usarem!
Raynos

1
._Deferredé simplesmente o verdadeiro "objeto adiado" que .Deferredutiliza. É um objeto interno do qual você provavelmente nunca precisará.
David Tang

Respostas:


212

O melhor caso de uso em que posso pensar é em cache de respostas AJAX. Aqui está um exemplo modificado da postagem de introdução de Rebecca Murphey sobre o tópico :

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

Basicamente, se o valor já tiver sido solicitado uma vez antes de ser retornado imediatamente do cache. Caso contrário, uma solicitação AJAX busca os dados e os adiciona ao cache. O $.when/ .thennão se importa com nada disso; tudo o que você precisa se preocupar é em usar a resposta, que é passada ao .then()manipulador nos dois casos. jQuery.when()lida com um que não seja Promessa / Adiado como Concluído, executando imediatamente qualquer um .done()ou .then()na cadeia.

Os adiados são perfeitos para quando a tarefa pode ou não operar de forma assíncrona e você deseja abstrair essa condição do código.

Outro exemplo do mundo real usando o $.whenauxiliar:

$.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
Dois exemplos brilhantes. Eu implementei algo semelhante ao segundo, mas com 4 solicitações de ajax, e ele tem um bom desempenho, além de ser muito mais legível, compacto, lógico, sustentável, etc. jQuery.Deferred é uma coisa muito boa.
RPP

5
Aqui está um vídeo úteis sobre este tema bigbinary.com/videos/3-using-deferred-in-jquery
Nick Vanderbilt

5
O armazenamento em cache não funcionará se o resultado for um valor falso. Também não gosto do fato de o getData retornar 2 tipos diferentes, dependendo do ramo utilizado.
Marko Dumic 27/03

3
Veja a resposta de Julian D. abaixo para uma melhor implementação do cache de ajax.
event_jr

1
Eu não entendo como o primeiro exemplo de código funciona: eu entendo o caso em que o objeto não é armazenado em cache, mas se ele não cache[ val ]retornar uma promessa (a documentação do jquery diz que o parâmetro são os dados retornados pelo remetente), o que significa que o acesso dos membros do .thenerro ... certo? o que estou perdendo?
precisa saber é o seguinte

79

Aqui está uma implementação ligeiramente diferente de um cache AJAX, como na resposta do ehynd .

Conforme observado na pergunta de acompanhamento do fortuneRice, a implementação do ehynd na verdade não impediu várias solicitações idênticas se as solicitações fossem executadas antes de um deles retornar. Isso é,

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

provavelmente resultará em 3 solicitações AJAX se o resultado para "xxx" ainda não tiver sido armazenado em cache antes.

Isso pode ser resolvido armazenando em cache os adiados da solicitação em vez do 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.
});

1
Eu acho que isso ainda não é perfeito, pois você nunca limpa / atualiza o cache uma vez buscado pela primeira vez. Isso fará com que a chamada AJAX não funcione para qualquer atualização.
zyzyis

45

Um adiado pode ser usado no lugar de um mutex. É basicamente o mesmo que os vários cenários de uso do ajax.

MUTEX

var mutex = 2;

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

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

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

DEFERIDO

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

Ao usar um adiado como apenas um mutex, cuidado com os impactos no desempenho (http://jsperf.com/deferred-vs-mutex/2). Embora a conveniência, bem como os benefícios adicionais fornecidos por um diferido, valham a pena, e no uso real (baseado em eventos do usuário), o impacto no desempenho não deve ser perceptível.


Foi surpreendentemente difícil para mim encontrar isso. Usei-o em uma função contendo um setInterval que retornaria a promessa resolvida e se autodestruiria uma vez que a largura de div chegasse a um determinado número. Era para solução de problemas e solução, se eu não conseguisse resolver meu problema, mas estou extasiado com isso.
JSG


20

Outro uso que venho colocando com bons objetivos é buscar dados de várias fontes. No exemplo abaixo, estou buscando vários objetos de esquema JSON independentes usados ​​em um aplicativo existente para validação entre um cliente e um servidor REST. Nesse caso, não quero que o aplicativo do lado do navegador comece a carregar dados antes de carregar todos os esquemas. $ .when.apply (). then () é perfeito para isso. Agradeço a Raynos pelas dicas sobre como usar (fn1, fn2) para monitorar condições de erro.

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

Outro exemplo usando Deferreds para implementar um cache para qualquer tipo de computação (geralmente algumas tarefas que exigem muito desempenho ou que demoram muito tempo):

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

Aqui está um exemplo de uso dessa classe para executar alguns 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);
});

O mesmo cache subjacente pode ser usado para armazenar em cache solicitações 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);
});

Você pode jogar com o código acima neste jsFiddle .


9

1) Use-o para garantir uma execução ordenada de retornos de chamada:

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) Use-o para verificar o status do aplicativo:

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

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

2

Você pode usar um objeto adiado para criar um design fluido que funcione bem em navegadores de kit da web. Os navegadores Webkit acionam o evento de redimensionamento para cada pixel em que a janela é redimensionada, ao contrário do FF e do IE, que acionam o evento apenas uma vez para cada redimensionamento. Como resultado, você não tem controle sobre a ordem em que as funções vinculadas ao seu evento de redimensionamento da janela serão executadas. Algo assim resolve o 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);
});

Isso serializará a execução do seu código para que ele seja executado como você deseja. Cuidado com as armadilhas ao passar métodos de objeto como retornos de chamada para um adiado. Depois que esse método for executado como um retorno de chamada para adiado, a referência 'this' será substituída pela referência ao objeto adiado e não se referirá mais ao objeto ao qual o método pertence.


Como isso faz alguma serialização? Você já resolveu a fila e resizeQueue.done(resizeAlgorithm)é exatamente o mesmo que resizeAlgorithm. É uma farsa completa!
Raynos

Quando o código do seu resizeAlgorithm é complexo, a implementação do JavaScript no webkit perde a sincronização quando a função é chamada para cada pixel que você redimensiona a janela. O adiado mantém seus retornos de chamada em uma fila e os executa em uma ordem FIFO. Portanto, se você adicionar um retorno de chamada 'pronto' e ele for executado imediatamente porque o adiado já foi resolvido, outro retorno de chamada 'pronto' que será adicionado ao adiado enquanto o primeiro retorno ainda estiver em execução será adicionado à fila e terá que esperar para o primeiro retorno de chamada retornar. Espero que isso responda à sua pergunta.
Miloš Rašić

o interpretador JS no navegador é único encadeado. A menos que seu resizeAlgorithm tenha algum código assíncrono, toda a função deve ter terminado de funcionar antes que a próxima chamada .doneseja feita.
Raynos

@ Raynos: Estou ciente disso, mas tentei simplesmente chamar o resizeAlgorithm no redimensionamento e ele fornece uma página em branco nos navegadores do kit da Web, enquanto trabalha perfeitamente em outros. O adiado resolve esse problema. Não tive tempo suficiente para fazer uma pesquisa mais profunda sobre isso. Pode ser um bug do webkit. Eu não acho que o adiado, conforme usado no meu exemplo, ajudaria se o redimensionar algoritmo tivesse algum código assíncrono.
Miloš Rašić

2
Você não deve usar algo como o plugin throttle / debounce benalman.com/projects/jquery-throttle-debounce-plugin para impedir que suas funções disparem mais de uma vez por redimensionamento.
wheresrhys

2

Você também pode integrá-lo a qualquer biblioteca de terceiros que faça uso do JQuery.

Uma dessas bibliotecas é o Backbone, que realmente suporta o Adiado na próxima versão.


2
Use read more hereno lugar de on my blog. É uma prática melhor e pode evitar que você responda (acidentalmente) a receber spam. :)
Lokesh Mehra

1

Acabei de usar adiado em código real. No projeto jQuery Terminal , tenho a função exec que chama comandos definidos pelo usuário (como se ele estivesse inserindo e pressionando enter), adicionei adiados à API e chamo exec com matrizes. como isso:

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

os comandos podem executar código assíncrono e o executivo precisa chamar o código do usuário em ordem. Minha primeira API usa um par de chamadas de pausa / retomada e na nova API eu as chamo automaticamente quando o usuário retorna a promessa. Portanto, o código do usuário pode apenas usar

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

ou

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

Eu uso 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 é usado na função resume que chama exec novamente com todos os dalyed_commands.

e parte da função de comandos (removi partes não 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();
        }
    }
}

1

A resposta por ehynds não funcionará, porque armazena em cache os dados das respostas. Ele deve armazenar em cache o jqXHR, que também é uma promessa. Aqui está o código correto:

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

A resposta de Julian D. funcionará corretamente e é uma solução melhor.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.