Passa un array di differiti a $ .when ()


447

Ecco un esempio inventato di ciò che sta succedendo: 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>");
        });
    });
});

Voglio "Tutto fatto!" apparire dopo che tutte le attività posticipate sono state completate, ma $.when()non sembra sapere come gestire una matrice di oggetti differiti. "Tutto fatto!" sta accadendo per primo perché l'array non è un oggetto differito, quindi jQuery procede e presuppone che sia stato appena fatto.

So che uno potrebbe passare gli oggetti nella funzione come, $.when(deferred1, deferred2, ..., deferredX)ma non si sa quanti oggetti rinviati ci saranno durante l'esecuzione nel problema reale che sto cercando di risolvere.



Aggiunta una nuova, più semplice, risposta per questa domanda molto vecchia di seguito. Non è necessario utilizzare un array o $.when.applyaffatto per ottenere lo stesso risultato.
Codice finito

ripristinato argomento della domanda, poiché era troppo specifico (questo non è solo un problema AJAX)
Alnitak,

Risposte:


732

Per passare una matrice di valori a qualsiasi funzione che normalmente si aspetta che siano parametri separati, utilizzare Function.prototype.apply, quindi in questo caso è necessario:

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

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

In ES6, invece, è possibile utilizzare l' ... operatore spread :

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

In entrambi i casi, poiché è improbabile che tu sappia in anticipo quanti parametri formali .thensaranno richiesti dal gestore, quel gestore dovrà elaborare l' argumentsarray per recuperare il risultato di ogni promessa.


4
Funziona a meraviglia. :) Sono sbalordito dal fatto che non sono stato in grado di ottenere un cambiamento così semplice tramite Google!
adamjford,

9
questo perché è un metodo generico, non specifico di $.when- f.apply(ctx, my_array)chiamerà fcon this == ctxe gli argomenti impostati sul contenuto di my_array.
Alnitak,

4
@Alnitak: sono un po 'imbarazzato di non conoscere quel metodo, considerando da quanto tempo scrivo JavaScript adesso!
adamjford,

5
FWIW, il link nella risposta di Eli a una domanda iniziale con la discussione del passaggio $vs nullcome primo parametro merita una lettura. In questo caso particolare, tuttavia, non importa.
Alnitak,

4
@Alnitak: Sì, ma $digita meno di nulle sei al sicuro quando l' $.whenimplementazione cambia (non che sia probabile in questo caso, ma perché non rimanere thisinvariato per impostazione predefinita).
Tomasz Zieliński,

109

Le soluzioni alternative sopra (grazie!) Non risolvono correttamente il problema di recuperare gli oggetti forniti al resolve()metodo differito perché jQuery chiama done()e fail()callback con parametri individuali, non un array. Ciò significa che dobbiamo usare lo argumentspseudo-array per ottenere tutti gli oggetti risolti / rifiutati restituiti dall'array di differiti, il che è brutto:

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

Dato che abbiamo passato una serie di rinvii, sarebbe bello recuperare una serie di risultati. Sarebbe anche bello recuperare un array reale anziché uno pseudo-array in modo da poter usare metodi come Array.sort().

Ecco una soluzione ispirata when.js 's when.all()metodo che affronta questi problemi:

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

Ora puoi semplicemente passare un array di rinvii / promesse e recuperare un array di oggetti risolti / rifiutati nel tuo callback, in questo modo:

$.when.all(deferreds).then(function(objects) {
    console.log("Resolved objects:", objects);
});

6
Potresti voler usare resolWith e rejectWith solo per ottenere gli stessi differiti originali di 'this' deferred.resolveWith (this, [Array.prototype.slice.call (argomenti)]) etc
Jamie Pate

1
C'è solo un piccolo problema con il tuo codice, quando c'è un solo elemento nell'array l'array dei risultati restituisce solo quel risultato, invece di un array con un singolo elemento (che interromperà il codice che prevede un array). Per risolverlo, utilizzare questa funzione var toArray = function (args) { return deferreds.length > 1 ? $.makeArray(args) : [args]; }anziché Array.prototype.slice.call.
Luan Nico,

Hm, questo non sembra catturare alcun 404.
t.mikael.d,

Trovato il motivo, .fail dovrebbe essere invece .reject - in modo che possa catturare 404.
t.mikael.d,

38

È possibile applicare il whenmetodo all'array:

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

$.when.apply($, arr);

Come si lavora con una matrice di jQuery Deferreds?


In realtà ho visto quella domanda ma immagino che tutti i dettagli extra in quella domanda abbiano fatto sì che la risposta al mio problema (che era proprio lì dentro) volasse proprio sopra la mia testa.
adamjford,

1
@adamjford, se ti fa sentire meglio, ho trovato la tua domanda più facile da consumare (e prima nella mia particolare ricerca su Google per questo problema esatto).
Patridge,

@patridge: felice di sapere che ti ha aiutato!
adamjford,

Questa è un'ottima risposta, ma non mi è chiaro come questo si sia applicato all'esempio nella domanda originale. Dopo aver consultato la domanda collegata, è diventato chiaro che la riga "$ .when (deferreds) .done (function () {" dovrebbe essere semplicemente cambiata in "$ .when.apply ($, deferreds) .done (function () { ". Giusto?
Ghirlanda Papa,

7

Quando si chiamano più chiamate AJAX parallele, sono disponibili due opzioni per gestire le rispettive risposte.

  1. Usa chiamata AJAX sincrona / uno dopo l'altro / non raccomandato
  2. Utilizzare l' Promises'array e $.whenche accetta promisese .doneviene richiamato il suo callback quando tutti gli promises vengono restituiti correttamente con le rispettive risposte.

Esempio

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>


1
la tua risposta è eccessiva, così come la tua modifica al titolo della domanda. L'OP sapeva già come effettuare le chiamate AJAX e ottenere una matrice di oggetti differiti. L' unico punto della domanda era come passare quella matrice a $.when.
Alnitak,

5
Ho pensato che spiegare in dettaglio con l'esempio sarebbe stato meglio, con le opzioni disponibili. E per questo non penso che il downvote fosse necessario.
Vinayakj,

2
il downvote era per 1. anche suggerire la sincronizzazione (anche se con una raccomandazione di non farlo) 2. il codice di scarsa qualità negli esempi (incluso for ... insu un array ?!)
Alnitak

1
1. D'accordo, avrebbe dovuto avere (not recommended)2.Non sono d'accordo - for ... inva bene perché l'array contiene solo quelle proprietà che necessitano (nessuna proprietà extra). grazie comunque
vinayakj

1
ri: 2 - il problema è che potrebbe essere copiato da altre persone che non sono in grado di garantire tale garanzia o che sono state abbastanza stupide da aggiungere Array.prototype. In ogni caso, per un codice non critico per le prestazioni sarebbe meglio usare al .mapposto di un for/ pushloop, ad es var promises = capitalCities.map(ajaxRequest); $.when.apply($, promises).then(fillCountryCapitals). Lavoro svolto.
Alnitak,

6

Come alternativa semplice, che non richiede $.when.applyo an array, è possibile utilizzare il modello seguente per generare un'unica promessa per più promesse parallele:

promise = $.when(promise, anotherPromise);

per esempio

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

Appunti:

  • Ho capito questo dopo aver visto le promesse di una catena in sequenza, usando promise = promise.then(newpromise)
  • Il rovescio della medaglia è che crea oggetti promettenti in più dietro le quinte e tutti i parametri passati alla fine non sono molto utili (poiché sono nidificati all'interno di oggetti aggiuntivi). Per quello che vuoi anche se è breve e semplice.
  • Il lato positivo è che non richiede alcun array o gestione dell'array.

2
Correggimi se sbaglio, ma il tuo approccio sta effettivamente annidando $ .when ($ .when ($ .when (...))) in modo da finire annidato ricorsivamente 10 livelli in profondità se ci sono 10 iterazioni. Questo non sembra molto parallelo poiché devi attendere che ogni livello restituisca la promessa annidata di un bambino prima che possa restituire la sua stessa promessa - Penso che l'approccio di matrice nella risposta accettata sia molto più pulito in quanto utilizza il comportamento flessibile dei parametri incorporato in il metodo $ .when ().
Anthony McLin,

@AnthonyMcLin: questo ha lo scopo di fornire un'alternativa più semplice alla codifica, non prestazioni migliori (che sono trascurabili con la maggior parte della codifica Async), come avviene per concatenare le then()chiamate in modo simile. Il comportamento con $.whendeve agire in quanto parallelo (non incatenato). Per favore, provalo prima di buttare via un'alternativa utile perché funziona :)
Gone Coding

2
@Alnitak: cavalli per corsi. Hai certamente diritto a un'opinione, ma ovviamente non l'hai usata tu stesso. La mia opinione si basa su usi pratici di questa tecnica. Si lavora e ha usi, quindi perché buttare via uno strumento dalla casella degli strumenti sulla base di esagerazioni come "carichi di caveat" (uno) e "non risolve nulla" (non è vero - si elimina l'elaborazione di array e semplifica concatenamento delle promesse parallele in cui il ritorno non sono necessari valori, che come dovresti sapere raramente vengono utilizzati in casi di elaborazione paralleli). I voti negativi dovrebbero essere per "questa risposta non è utile" :)
Gone Coding

1
Ciao @GoneCoding. Posso chiederti di non aggiungere commenti di voto alle tue risposte? Questo è adatto per i commenti, ma per il resto è il rumore che distrae dal contenuto altrimenti buono. Grazie.
halfer

1
@halfer: non pubblico più, ma sono infastidito dall'ignoranza mostrata a qualcosa di originale. Mantenere tutte le nuove idee per me al giorno d'oggi :)
Gone Coding

4

Voglio proporne un altro usando $ .each:

  1. Possiamo dichiarare la funzione Ajax come:

    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 del codice in cui creiamo una serie di funzioni con ajax da inviare:

    var arrayOfFn = [];
    for (var i = 0; i < someDataArray.length; i++) {
        var ajaxFnForArray = new ajaxFn(someDataArray[i]);
        arrayOfFn.push(ajaxFnForArray);
    }
  3. E chiamare le funzioni con l'invio di ajax:

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

1

Se esegui la transpiling e hai accesso a ES6, puoi utilizzare la sintassi di spread che applica in modo specifico ogni elemento iterabile di un oggetto come argomento discreto, nel modo giusto $.when().

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

Collegamento MDN - Sintassi diffusa


0

Se stai usando angularJS o qualche variante della libreria Q promise, hai un .all()metodo che risolve questo esatto problema.

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

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

vedi l'API completa:

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

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


4
Questo è completamente irrilevante.
Benjamin Gruenbaum,

@BenjaminGruenbaum In che modo? Tutte le librerie di promesse javascript condividono un'API simile e non c'è nulla di sbagliato nel mostrare le diverse implementazioni. Ho raggiunto questa pagina in cerca di una risposta angolare e sospetto che molti altri utenti raggiungeranno questa pagina e non si troveranno necessariamente in un ambiente solo jquery.
mastaBlasta,

2
Vale a dire, poiché le promesse di jQuery non condividono questa API, questa è completamente inappropriata come risposta su Stack Overflow: ci sono risposte simili per Angular e puoi chiedere lì. (Per non parlare, dovresti .mapqui ma vabbè).
Benjamin Gruenbaum,

0

Ho avuto un caso molto simile in cui stavo postando in ogni ciclo e quindi impostando il markup html in alcuni campi dai numeri ricevuti da ajax. Avevo quindi bisogno di fare una somma dei valori (ora aggiornati) di questi campi e posizionarli in un campo totale.

Quindi il problema era che stavo cercando di fare una somma su tutti i numeri ma non erano ancora arrivati ​​dati dalle chiamate asincrone ajax. Avevo bisogno di completare questa funzionalità in alcune funzioni per poter riutilizzare il codice. La mia funzione esterna attende i dati prima che io poi vada a fare alcune cose con il DOM completamente aggiornato.

    // 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;
    }
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.