Passe uma matriz de adiados para $ .when ()

447

Aqui está um exemplo artificial do que está acontecendo: http://jsfiddle.net/adamjford/YNGcm/20/

HTML:

<a href="#">Click me!</a>
<div></div>

JavaScript:

function getSomeDeferredStuff() {
    var deferreds = [];

    var i = 1;
    for (i = 1; i <= 10; i++) {
        var count = i;

        deferreds.push(
        $.post('/echo/html/', {
            html: "<p>Task #" + count + " complete.",
            delay: count
        }).success(function(data) {
            $("div").append(data);
        }));
    }

    return deferreds;
}

$(function() {
    $("a").click(function() {
        var deferreds = getSomeDeferredStuff();

        $.when(deferreds).done(function() {
            $("div").append("<p>All done!</p>");
        });
    });
});

Eu quero "Tudo pronto!" apareça após a conclusão de todas as tarefas adiadas, mas $.when()não parece saber como lidar com uma matriz de objetos adiados . "Tudo feito!" está acontecendo primeiro porque o array não é um objeto Adiado, portanto, o jQuery segue em frente e assume que acabou de terminar.

Eu sei que é possível passar os objetos para a função, $.when(deferred1, deferred2, ..., deferredX)mas não se sabe quantos objetos adiados haverá em execução no problema real que estou tentando resolver.

adamjford
fonte
Adicionada uma resposta nova e mais simples para esta pergunta muito antiga abaixo. Você não precisa usar uma matriz ou $.when.applyobter o mesmo resultado.
Gone Coding
revertida questão assunto, como era muito específico (este não é apenas um problema de AJAX)
Alnitak

Respostas:

732

Para passar uma matriz de valores para qualquer função que normalmente espera que sejam parâmetros separados, use Function.prototype.apply, portanto, neste caso, você precisa:

$.when.apply($, my_array).then( ___ );

Consulte http://jsfiddle.net/YNGcm/21/

No ES6, você pode usar o ... operador de spread :

$.when(...my_array).then( ___ );

Em qualquer um dos casos, como é improvável que você saiba com antecedência quantos parâmetros formais o .thenmanipulador precisará, esse manipulador precisaria processar a argumentsmatriz para recuperar o resultado de cada promessa.

Alnitak
fonte
4
Isso funciona, incrível. :) Estou surpreso por não ter conseguido realizar uma mudança tão simples via Google!
adamjford
9
isso porque é um método genérico, não específico para $.when- f.apply(ctx, my_array)irá chamar fcom this == ctxe os argumentos definidos para o conteúdo de my_array.
Alnitak
4
@Alnitak: Estou um pouco envergonhado por não saber sobre esse método, considerando há quanto tempo estou escrevendo JavaScript agora!
adamjford
5
FWIW, o link na resposta de Eli para uma pergunta inicial com a discussão sobre passar $vs nullcomo o primeiro parâmetro vale uma leitura. Nesse caso em particular, não importa.
Alnitak
4
@ Alnitak: Sim, mas $é menos digitado nulle você está seguro quando a $.whenimplementação é alterada (não é provável que neste caso, mas por que não manter thisinalterado por padrão).
Tomasz Zieliński
109

As soluções alternativas acima (obrigado!) Não abordam adequadamente o problema de retornar os objetos fornecidos ao resolve()método do adiado, porque o jQuery chama os retornos de chamada done()e fail()com parâmetros individuais, não uma matriz. Isso significa que precisamos usar o argumentspseudo-array para obter todos os objetos resolvidos / rejeitados retornados pelo array de diferidos, o que é feio:

$.when.apply($,deferreds).then(function() {
     var objects=arguments; // The array of resolved objects as a pseudo-array
     ...
};

Como passamos uma série de adiados, seria bom recuperar uma série de resultados. Também seria bom recuperar uma matriz real em vez de uma pseudo-matriz, para que possamos usar métodos como Array.sort().

Aqui está uma solução inspirada por when.js 's when.all()método que aborda estes problemas:

// Put somewhere in your scripting environment
if (typeof jQuery.when.all === 'undefined') {
    jQuery.when.all = function (deferreds) {
        return $.Deferred(function (def) {
            $.when.apply(jQuery, deferreds).then(
                function () {
                    def.resolveWith(this, [Array.prototype.slice.call(arguments)]);
                },
                function () {
                    def.rejectWith(this, [Array.prototype.slice.call(arguments)]);
                });
        });
    }
}

Agora você pode simplesmente passar uma matriz de adiados / promessas e recuperar uma matriz de objetos resolvidos / rejeitados em seu retorno de chamada, assim:

$.when.all(deferreds).then(function(objects) {
    console.log("Resolved objects:", objects);
});
Pato crocante
fonte
6
Você pode querer usar resolveWith e rejeitarWith apenas para obter os mesmos diferidos originais que 'this' deferred.resolveWith (this, [Array.prototype.slice.call (argumentos)]) etc.
Jamie Pate
1
Há apenas um pequeno problema com seu código, quando há apenas um elemento na matriz, a matriz de resultados retorna apenas esse resultado, em vez de uma matriz com um único elemento (que interromperá o código que espera uma matriz). Para corrigi-lo, use esta função em var toArray = function (args) { return deferreds.length > 1 ? $.makeArray(args) : [args]; }vez de Array.prototype.slice.call.
Luan Nico
Hum, isso não parece pegar nenhum 404.
t.mikael.d
Encontrado o motivo, .fail deve ser .reject - para que ele possa capturar 404.
t.mikael.d
38

Você pode aplicar o whenmétodo à sua matriz:

var arr = [ /* Deferred objects */ ];

$.when.apply($, arr);

Como você trabalha com uma matriz de adiados jQuery?

Eli
fonte
Na verdade, eu vi essa pergunta, mas acho que todos os detalhes extras dessa pergunta fizeram com que a resposta do meu problema (que estava ali) voasse sobre minha cabeça.
adamjford
1
@ adamjford, se isso te faz sentir melhor, achei sua pergunta mais fácil de consumir (e primeiro na minha pesquisa particular no Google por esse problema exato).
patridge
@patridge: Feliz em saber que isso ajudou você!
adamjford
Essa é uma ótima resposta, mas não estava claro para mim como isso se aplicava ao exemplo na pergunta original. Após consultar a questão vinculada, ficou claro que a linha "$ .when (adiada). Concluída (function () {" deveria ser simplesmente alterada para "$ .when.apply ($, adiada). Concluída (function () { ". Certo?
Garland Pope
7

Ao chamar várias chamadas paralelas AJAX, você tem duas opções para manipular as respectivas respostas.

  1. Usar chamada AJAX síncrona / uma após a outra / não recomendado
  2. Use Promises'array e $.whenque aceite promises e seu retorno de chamada .doneseja chamado quando todos os promises retornarem com êxito com as respectivas respostas.

Exemplo

function ajaxRequest(capitalCity) {
   return $.ajax({
        url: 'https://restcountries.eu/rest/v1/capital/'+capitalCity,
        success: function(response) {
        },
        error: function(response) {
          console.log("Error")
        }
    });
}
$(function(){
   var capitalCities = ['Delhi', 'Beijing', 'Washington', 'Tokyo', 'London'];
   $('#capitals').text(capitalCities);

   function getCountryCapitals(){ //do multiple parallel ajax requests
      var promises = [];   
      for(var i=0,l=capitalCities.length; i<l; i++){
            var promise = ajaxRequest(capitalCities[i]);
            promises.push(promise);
      }
  
      $.when.apply($, promises)
        .done(fillCountryCapitals);
   }
  
   function fillCountryCapitals(){
        var countries = [];
        var responses = arguments;
        for(i in responses){
            console.dir(responses[i]);
            countries.push(responses[i][0][0].nativeName)
        }  
        $('#countries').text(countries);
   }
  
   getCountryCapitals()
})
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div>
  <h4>Capital Cities : </h4> <span id="capitals"></span>
  <h4>Respective Country's Native Names : </h4> <span id="countries"></span>
</div>

vinayakj
fonte
1
sua resposta ultrapassa e sua edição no título da pergunta. O OP já sabia como fazer chamadas AJAX e obter uma matriz de objetos adiados. O único ponto da questão era como passar essa matriz para $.when.
Alnitak
5
Eu pensei que explicar detalhadamente com o exemplo seria melhor, com as opções disponíveis.
vinayakj
2
o downvote foi para 1. até sugerindo sincronização (embora com uma recomendação de não) 2. o código de má qualidade nos exemplos (incluindo for ... inem uma matriz ?!)
Alnitak
1
1. Concordo, deveria ter tido (not recommended)2.Não concordo - for ... inestá ok porque a matriz contém apenas as propriedades necessárias (sem propriedades extras). obrigado de qualquer maneira
vinayakj
1
re: 2 - o problema é que ele pode ser copiado por outras pessoas que não podem dar essa garantia ou que foram burros o suficiente para adicionar isso Array.prototype. De qualquer forma, para código que não seja crítico para o desempenho, seria melhor usar em .mapvez de um loop for/ push, por exemplo var promises = capitalCities.map(ajaxRequest); $.when.apply($, promises).then(fillCountryCapitals)- trabalho concluído.
Alnitak
6

Como uma alternativa simples, que não exige $.when.applyou arrayvocê pode usar o seguinte padrão para gerar uma única promessa para várias promessas paralelas:

promise = $.when(promise, anotherPromise);

por exemplo

function GetSomeDeferredStuff() {
    // Start with an empty resolved promise (or undefined does the same!)
    var promise;
    var i = 1;
    for (i = 1; i <= 5; i++) {
        var count = i;

        promise = $.when(promise,
        $.ajax({
            type: "POST",
            url: '/echo/html/',
            data: {
                html: "<p>Task #" + count + " complete.",
                delay: count / 2
            },
            success: function (data) {
                $("div").append(data);
            }
        }));
    }
    return promise;
}

$(function () {
    $("a").click(function () {
        var promise = GetSomeDeferredStuff();
        promise.then(function () {
            $("div").append("<p>All done!</p>");
        });
    });
});

Notas:

  • Eu descobri isso depois de ver alguém prometer em cadeia sequencialmente, usando promise = promise.then(newpromise)
  • A desvantagem é que ele cria objetos de promessa extra nos bastidores e quaisquer parâmetros passados ​​no final não são muito úteis (pois estão aninhados dentro de objetos adicionais). Para o que você quer, embora seja curto e simples.
  • O lado positivo é que ele não requer matriz ou gerenciamento de matriz.
Gone Coding
fonte
2
Me corrija se eu estiver errado, mas sua abordagem está efetivamente aninhando $ .when ($ .when ($ .when (...))) para que você acabe aninhando recursivamente 10 níveis de profundidade se houver 10 iterações. Isso não parece muito paralelo, pois você precisa esperar que cada nível retorne a promessa aninhada de uma criança antes que ela possa retornar sua própria promessa - acho que a abordagem de matriz na resposta aceita é muito mais limpa, pois usa o comportamento do parâmetro flexível incorporado o método $ .when ().
Anthony McLin
@AnthonyMcLin: tem como objetivo fornecer uma alternativa mais simples à codificação, não melhor desempenho (que é desprezível na maioria das codificações Async), como é feito com as then()chamadas em cadeia de maneira semelhante. O comportamento com $.whené agir como é paralelo (não encadeado). Por favor, tente-lo antes de jogar fora uma alternativa útil, pois funciona :)
Ido Codificação
2
@ Alnitak: Cavalos para cursos. Você certamente tem direito a uma opinião, mas obviamente não a usou. Minha opinião é baseada em usos práticos dessa técnica. Ele trabalha e tem usos, então por que jogar fora uma ferramenta da caixa de ferramentas com base em exageros como "cargas de caveats" (um) e "não resolve nada" (não é verdade - que elimina o processamento de matriz e simplifica encadeamento das promessas paralelas, onde o retorno não são necessários valores que, como você deve saber, raramente são usados ​​em casos de processamento paralelo). Downvotes são supostamente para "esta resposta não é útil" :)
Ido Codificação
1
Olá, @GoneCoding. Posso pedir que você não adicione comentários de votação às suas respostas? Isso é adequado para comentários, mas, caso contrário, é o ruído que distrai o conteúdo bom. Obrigado.
halfer
1
@ halfer: Eu não posto mais, mas estou irritado com a ignorância exibida em algo original. Mantendo todas as idéias novas para mim hoje em dia :)
Ido Codificação
4

Quero propor outro usando $ .each:

  1. Podemos declarar a função ajax como:

    function ajaxFn(someData) {
        this.someData = someData;
        var that = this;
        return function () {
            var promise = $.Deferred();
            $.ajax({
                method: "POST",
                url: "url",
                data: that.someData,
                success: function(data) {
                    promise.resolve(data);
                },
                error: function(data) {
                    promise.reject(data);
                }
            })
            return promise;
        }
    }
  2. Parte do código onde criamos uma matriz de funções com o ajax para enviar:

    var arrayOfFn = [];
    for (var i = 0; i < someDataArray.length; i++) {
        var ajaxFnForArray = new ajaxFn(someDataArray[i]);
        arrayOfFn.push(ajaxFnForArray);
    }
  3. E chamando funções com o envio de ajax:

    $.when(
        $.each(arrayOfFn, function(index, value) {
            value.call()
        })
    ).then(function() {
            alert("Cheer!");
        }
    )
Volodymyr Yasinskyi
fonte
1

Se você estiver transpilando e tiver acesso ao ES6, poderá usar a sintaxe de propagação, que aplica especificamente cada item iterável de um objeto como um argumento discreto, da maneira que for $.when()necessária.

$.when(...deferreds).done(() => {
    // do stuff
});

Link MDN - Sintaxe de propagação

relíquia
fonte
0

Se você estiver usando angularJS ou alguma variante da biblioteca Q promessa, então você tem um .all()método que resolve esse problema exato.

var savePromises = [];
angular.forEach(models, function(model){
  savePromises.push(
    model.saveToServer()
  )
});

$q.all(savePromises).then(
  function success(results){...},
  function failed(results){...}
);

veja a API completa:

https://github.com/kriskowal/q/wiki/API-Reference#promiseall

https://docs.angularjs.org/api/ng/service/$q

mastaBlasta
fonte
4
Isso é completamente irrelevante.
Benjamin Gruenbaum
@BenjaminGruenbaum Como assim? Todas as bibliotecas de promessas javascript compartilham uma API semelhante e não há nada errado em mostrar as diferentes implementações. Cheguei a esta página procurando uma resposta para angular, e suspeito que muitos outros usuários cheguem a essa página e não necessariamente estejam em um ambiente apenas de jquery.
mastaBlasta
2
Ou seja, como as promessas do jQuery não compartilham essa API, isso é completamente inapropriado como resposta no Stack Overflow - há respostas semelhantes para o Angular e você pode perguntar lá. (Sem mencionar, você deveria estar .mapaqui, mas tudo bem).
Benjamin Gruenbaum
0

Eu tive um caso muito parecido em que eu estava postando em cada loop e, em seguida, definindo a marcação html em alguns campos dos números recebidos do ajax. Eu então precisava fazer uma soma dos valores (agora atualizados) desses campos e colocar em um campo total.

Assim, o problema era que eu estava tentando fazer uma soma em todos os números, mas nenhum dado havia chegado ainda das chamadas assíncronas ajax. Eu precisava concluir essa funcionalidade em algumas funções para poder reutilizar o código. Minha função externa aguarda os dados antes de eu fazer algumas coisas com o DOM totalmente atualizado.

    // 1st
    function Outer() {
        var deferreds = GetAllData();

        $.when.apply($, deferreds).done(function () {
            // now you can do whatever you want with the updated page
        });
    }

    // 2nd
    function GetAllData() {
        var deferreds = [];
        $('.calculatedField').each(function (data) {
            deferreds.push(GetIndividualData($(this)));
        });
        return deferreds;
    }

    // 3rd
    function GetIndividualData(item) {
        var def = new $.Deferred();
        $.post('@Url.Action("GetData")', function (data) {
            item.html(data.valueFromAjax);
            def.resolve(data);
        });
        return def;
    }
Cameron Atacante
fonte