Pase una variedad de diferidos a $ .when ()

447

Aquí hay un ejemplo artificial de lo que está sucediendo: 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>");
        });
    });
});

Quiero "¡Todo listo!" aparecer después de que se hayan completado todas las tareas diferidas, pero $.when()no parece saber cómo manejar una matriz de objetos diferidos. "¡Todo listo!" está sucediendo primero porque la matriz no es un objeto diferido, por lo que jQuery sigue adelante y supone que se acaba de hacer.

Sé que uno podría pasar los objetos a la función, $.when(deferred1, deferred2, ..., deferredX)pero no se sabe cuántos objetos diferidos habrá en la ejecución del problema real que estoy tratando de resolver.

adamjford
fuente
Se agregó una respuesta nueva y más simple para esta antigua pregunta a continuación. Usted no necesita utilizar una matriz o $.when.applyen absoluto para obtener el mismo resultado.
Gone Coding
tema de pregunta revertido, ya que era demasiado específico (esto no es solo un problema AJAX)
Alnitak

Respuestas:

732

Para pasar una matriz de valores a cualquier función que normalmente espera que sean parámetros separados, use Function.prototype.apply, por lo que en este caso necesita:

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

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

En ES6, puede utilizar el ... operador de propagación en su lugar:

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

En cualquier caso, dado que es poco probable que sepa de antemano cuántos parámetros formales .thenrequerirá el controlador, ese controlador necesitará procesar la argumentsmatriz para recuperar el resultado de cada promesa.

Alnitak
fuente
44
Esto funciona, genial. :) ¡Estoy sorprendido de que no haya podido obtener un cambio tan simple a través de Google!
adamjford
99
eso se debe a que es un método genérico, no específico de $.when: f.apply(ctx, my_array)llamará fcon this == ctxy los argumentos se ajustarán al contenido de my_array.
Alnitak
44
@Alnitak: ¡Estoy un poco avergonzado de no saber sobre ese método, considerando cuánto tiempo he estado escribiendo JavaScript ahora!
adamjford
55
FWIW, vale la pena leer el enlace en la respuesta de Eli a una pregunta previa con una discusión sobre pasar $vs nullcomo primer parámetro. Sin embargo, en este caso particular no importa.
Alnitak
44
@Alnitak: Sí, pero $escribe menos nully está seguro cuando la $.whenimplementación cambia (no es probable que sea en este caso, pero por qué no se mantiene thissin cambios por defecto).
Tomasz Zieliński
109

Las soluciones anteriores (gracias!) No abordan adecuadamente el problema de volver a los objetos proporcionados a la del diferida resolve()método porque jQuery llama done()y fail()devoluciones de llamada con los parámetros individuales, no una matriz. Eso significa que tenemos que usar la argumentspseudo-matriz para obtener todos los objetos resueltos / rechazados devueltos por la matriz de diferidos, lo cual es feo:

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

Dado que aprobamos una serie de aplazamientos, sería bueno recuperar una serie de resultados. También sería bueno recuperar una matriz real en lugar de una pseudo-matriz para que podamos usar métodos como Array.sort().

Aquí hay una solución inspirada en el método de when.jswhen.all() que aborda estos 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)]);
                });
        });
    }
}

Ahora puede simplemente pasar una matriz de diferidos / promesas y recuperar una matriz de objetos resueltos / rechazados en su devolución de llamada, de esta manera:

$.when.all(deferreds).then(function(objects) {
    console.log("Resolved objects:", objects);
});
Pato crujiente
fuente
66
Es posible que desee utilizar resolveWith y rechazarWith solo para obtener los mismos aplazamientos originales que 'this' deferred.resolveWith (this, [Array.prototype.slice.call (argumentos)]) etc.
Jamie Pate
1
Solo hay un pequeño problema con su código, cuando solo hay un elemento en la matriz, la matriz de resultados devuelve solo ese resultado, en lugar de una matriz con un solo elemento (que romperá el código que espera una matriz). Para solucionarlo, use esta función en var toArray = function (args) { return deferreds.length > 1 ? $.makeArray(args) : [args]; }lugar de Array.prototype.slice.call.
Luan Nico
Hm, esto no parece captar ningún 404.
t.mikael.d
Encontró la razón, .fail debería ser .reject en su lugar, para que pueda atrapar 404's.
t.mikael.d
38

Puede aplicar el whenmétodo a su matriz:

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

$.when.apply($, arr);

¿Cómo trabaja con una variedad de jQuery Deferreds?

Eli
fuente
De hecho, vi esa pregunta, pero creo que todos los detalles adicionales en esa pregunta causaron que la respuesta a mi problema (que estaba allí) volara por encima de mi cabeza.
adamjford
1
@adamjford, si te hace sentir mejor, encontré tu pregunta más fácil de consumir (y primero en mi búsqueda particular de Google para este problema exacto).
patridge
@patridge: ¡Me alegra saber que te ayudó!
adamjford
Esta es una gran respuesta, pero no estaba claro para mí cómo se aplicaba al ejemplo en la pregunta original. Después de consultar la pregunta vinculada, quedó claro que la línea "$ .when (deferreds) .done (function () {" simplemente debería cambiarse a "$ .when.apply ($, deferreds) .done (function () { ". ¿Verdad?
Garland Pope,
7

Al llamar a varias llamadas AJAX paralelas, tiene dos opciones para manejar las respuestas respectivas.

  1. Usar llamada AJAX sincrónica / una tras otra / no recomendado
  2. Use Promises'array y $.whenque acepte promises y su devolución de llamada .donese llama cuando todos los promises se devuelven correctamente con las respuestas respectivas.

Ejemplo

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
fuente
1
su respuesta se extralimita, y también lo hizo su edición del título de la pregunta. El OP ya sabía cómo hacer las llamadas AJAX y obtener una matriz de objetos diferidos. El único punto de la pregunta era cómo pasar esa matriz a $.when.
Alnitak
55
Pensé que explicar en detalle con el ejemplo sería mejor, con las opciones disponibles, y para eso no creo que fuera necesario votar negativamente.
vinayakj
2
el voto negativo fue para 1. incluso sugiriendo sincronización (aunque con una recomendación de no hacerlo) 2. el código de mala calidad en los ejemplos (¡¿incluido for ... inen una matriz ?!)
Alnitak
1
1. De acuerdo, debería haber tenido (not recommended)2. No está de acuerdo - for ... inestá bien porque la matriz contiene solo aquellas propiedades que necesitan (sin propiedades adicionales). gracias de todos modos
vinayakj
1
re: 2 - el problema es que podría ser copiado por otras personas que no pueden hacer esa garantía, o que han sido lo suficientemente tontas como para agregarlo Array.prototype. En cualquier caso, para un código que no sea crítico para el rendimiento, sería mejor usarlo en .maplugar de un bucle for/ push, por ejemplo var promises = capitalCities.map(ajaxRequest); $.when.apply($, promises).then(fillCountryCapitals), trabajo realizado.
Alnitak
6

Como alternativa simple, que no requiere $.when.applyo una array, puede usar el siguiente patrón para generar una sola promesa para múltiples promesas paralelas:

promise = $.when(promise, anotherPromise);

p.ej

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:

  • Me di cuenta de esto después de ver a alguien encadenar promesas secuencialmente, usando promise = promise.then(newpromise)
  • La desventaja es que crea objetos de promesa adicionales detrás de escena y los parámetros pasados ​​al final no son muy útiles (ya que están anidados dentro de objetos adicionales). Para lo que quieres, aunque es corto y simple.
  • Lo bueno es que no requiere matriz o gestión de matriz.
Codificación ido
fuente
2
Corrígeme si me equivoco, pero tu enfoque está anidando efectivamente $ .when ($ .when ($ .when (...))) para que termines anidando recursivamente 10 niveles de profundidad si hay 10 iteraciones. Esto no parece muy paralelo, ya que debe esperar a que cada nivel devuelva la promesa anidada de un niño antes de que pueda devolver su propia promesa: creo que el enfoque de matriz en la respuesta aceptada es mucho más limpio, ya que utiliza el comportamiento de parámetros flexibles incorporado en el método $ .when ().
Anthony McLin
@AnthonyMcLin: está destinado a proporcionar una alternativa más simple a la codificación, no un mejor rendimiento (que es insignificante con la mayoría de la codificación asíncrona), como se hace con el encadenamiento de then()llamadas de manera similar. El comportamiento con $.whenes actuar como si fuera paralelo (no encadenado). Por favor, pruébalo antes de tirar una alternativa útil, ya que funciona :)
Gone Coding
2
@Alnitak: Caballos para cursos. Ciertamente tiene derecho a una opinión, pero obviamente no la ha utilizado usted mismo. Mi propia opinión se basa en usos prácticos de esta técnica. Se trabaja y tiene usos, ¿por qué tirar una herramienta de la caja de herramientas basado en exageraciones como "un montón de advertencias" (uno) y "no resuelve nada" (no es cierto - se elimina el procesamiento de señal y simplifica el encadenamiento de las promesas paralelas donde el retorno no se necesitan valores, que como debe saber, rara vez se usan en casos de procesamiento paralelo de todos modos). Se supone que los votos negativos son para "esta respuesta no es útil" :)
Gone Coding
1
Hola @GoneCoding ¿Puedo pedirle que no agregue comentarios de votación a sus respuestas? Eso es adecuado para comentarios, pero de lo contrario es el ruido lo que distrae del contenido bueno. Gracias.
halfer
1
@halfer: Ya no publico más, pero estoy molesto por la ignorancia que se muestra a todo lo original. Guardar todas las ideas nuevas para mí hoy en día :)
Gone Coding
4

Quiero proponer otro usando $ .each:

  1. Podemos declarar la función 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 del código donde creamos una matriz de funciones con ajax para enviar:

    var arrayOfFn = [];
    for (var i = 0; i < someDataArray.length; i++) {
        var ajaxFnForArray = new ajaxFn(someDataArray[i]);
        arrayOfFn.push(ajaxFnForArray);
    }
  3. Y llamando a funciones con el envío de ajax:

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

Si está transpilando y tiene acceso a ES6, puede usar la sintaxis extendida que aplica específicamente cada elemento iterable de un objeto como un argumento discreto, tal como lo $.when()necesita.

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

MDN Link - Sintaxis extendida

reliquia
fuente
0

Si está utilizando angularJS o alguna variante de la biblioteca de promesa Q, entonces tiene un .all()método que resuelve este problema exacto.

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

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

ver la API completa:

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

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

mastaBlasta
fuente
44
Esto es completamente irrelevante.
Benjamin Gruenbaum
@BenjaminGruenbaum ¿Cómo es eso? Todas las bibliotecas de promesa de JavaScript comparten una API similar, y no hay nada de malo en mostrar las diferentes implementaciones. Llegué a esta página en busca de una respuesta para angular, y sospecho que muchos otros usuarios llegarán a esta página y no necesariamente estarán en un entorno de jquery.
mastaBlasta
2
Es decir, debido a que las promesas de jQuery no comparten esta API, esto es completamente inapropiado como respuesta en Stack Overflow: hay respuestas similares para Angular y puede preguntar allí. (Sin mencionar que deberías estar .mapaquí, pero bueno).
Benjamin Gruenbaum
0

Tuve un caso muy similar en el que publicaba en cada bucle y luego configuraba el marcado html en algunos campos de los números recibidos del ajax. Luego necesitaba hacer una suma de los valores (ahora actualizados) de estos campos y colocarlos en un campo total.

Por lo tanto, el problema era que estaba tratando de hacer una suma de todos los números, pero aún no había recibido datos de las llamadas asíncronas ajax. Necesitaba completar esta funcionalidad en algunas funciones para poder reutilizar el código. Mi función externa espera los datos antes de ir y hacer algunas cosas con el DOM completamente actualizado.

    // 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 Delantero
fuente