devolución de llamada jQuery para múltiples llamadas ajax

132

Quiero hacer tres llamadas ajax en un evento de clic. Cada llamada ajax realiza una operación distinta y devuelve los datos necesarios para una devolución de llamada final. Las llamadas en sí no dependen una de la otra, todas pueden ir al mismo tiempo, sin embargo, me gustaría tener una devolución de llamada final cuando las tres estén completas.

$('#button').click(function() {
    fun1();
    fun2();
    fun3();
//now do something else when the requests have done their 'success' callbacks.
});

var fun1= (function() {
    $.ajax({/*code*/});
});
var fun2 = (function() {
    $.ajax({/*code*/});
});
var fun3 = (function() {
    $.ajax({/*code*/});
});
SeñorIsaak
fuente

Respuestas:

112

Aquí hay un objeto de devolución de llamada que escribí donde puede configurar una devolución de llamada única para que se active una vez que esté completa o dejar que cada una tenga su propia devolución de llamada y dispararlas todas una vez que estén completas:

AVISO

Desde jQuery 1.5+ puede usar el método diferido como se describe en otra respuesta:

  $.when($.ajax(), [...]).then(function(results){},[...]);

Ejemplo de diferido aquí

para jQuery <1.5 lo siguiente funcionará o si necesita que se activen sus llamadas ajax en momentos desconocidos, como se muestra aquí con dos botones: se activa después de hacer clic en ambos botones

[uso]

para devolución de llamada única una vez completada: ejemplo de trabajo

// initialize here
var requestCallback = new MyRequestsCompleted({
    numRequest: 3,
    singleCallback: function(){
        alert( "I'm the callback");
    }
});

//usage in request
$.ajax({
    url: '/echo/html/',
    success: function(data) {
        requestCallback.requestComplete(true);
    }
});
$.ajax({
    url: '/echo/html/',
    success: function(data) {
        requestCallback.requestComplete(true);
    }
});
$.ajax({
    url: '/echo/html/',
    success: function(data) {
        requestCallback.requestComplete(true);
    }
});

cada uno tiene su propia devolución de llamada cuando todo está completo: ejemplo de trabajo

//initialize 
var requestCallback = new MyRequestsCompleted({
    numRequest: 3
});

//usage in request
$.ajax({
    url: '/echo/html/',
    success: function(data) {
        requestCallback.addCallbackToQueue(true, function() {
            alert('Im the first callback');
        });
    }
});
$.ajax({
    url: '/echo/html/',
    success: function(data) {
        requestCallback.addCallbackToQueue(true, function() {
            alert('Im the second callback');
        });
    }
});
$.ajax({
    url: '/echo/html/',
    success: function(data) {
        requestCallback.addCallbackToQueue(true, function() {
            alert('Im the third callback');
        });
    }
});

[El código]

var MyRequestsCompleted = (function() {
    var numRequestToComplete, requestsCompleted, callBacks, singleCallBack;

    return function(options) {
        if (!options) options = {};

        numRequestToComplete = options.numRequest || 0;
        requestsCompleted = options.requestsCompleted || 0;
        callBacks = [];
        var fireCallbacks = function() {
            alert("we're all complete");
            for (var i = 0; i < callBacks.length; i++) callBacks[i]();
        };
        if (options.singleCallback) callBacks.push(options.singleCallback);

        this.addCallbackToQueue = function(isComplete, callback) {
            if (isComplete) requestsCompleted++;
            if (callback) callBacks.push(callback);
            if (requestsCompleted == numRequestToComplete) fireCallbacks();
        };
        this.requestComplete = function(isComplete) {
            if (isComplete) requestsCompleted++;
            if (requestsCompleted == numRequestToComplete) fireCallbacks();
        };
        this.setCallback = function(callback) {
            callBacks.push(callBack);
        };
    };
})();
subhaze
fuente
1
¡Gracias por el gran ejemplo de código! Creo que podré tropezarme con el resto. ¡Gracias de nuevo!
MisterIsaak
No hay problema, me alegro de que haya ayudado. Me dejé llevar un poco: todavía tengo algunas ideas más. Planeo actualizarlo donde no tenga que especificar una cantidad de solicitudes en la inicialización.
subhaze
Bien, espero que lo hagas! Muy interesante, sugeriría incluir la opción de agregar una devolución de llamada adicional. Lo hice y funciona perfecto para lo que quiero hacer. ¡Gracias de nuevo!
MisterIsaak
Lo hice pero olvidé agregarlo al caso de uso: P uso setCallbackpara agregar más a la cola. Aunque ahora que lo pienso ... debería llamarlo addCallbacko algo por el estilo ... y no solo configurarlo, ya que se está agregando a la cola
subhaze el
¿Es singleCallbackinnecesaria la variable en la segunda línea? ¿O me estoy perdiendo algo?
nimcap 01 de
158

Parece que tiene algunas respuestas a esto, sin embargo, creo que hay algo que vale la pena mencionar aquí que simplificará enormemente su código. jQuery presentó el$.when en v1.5. Parece que:

$.when($.ajax(...), $.ajax(...)).then(function (resp1, resp2) {
    //this callback will be fired once all ajax calls have finished.
});

No lo vi mencionado aquí, espero que ayude.

Greg
fuente
66
Gracias por la respuesta, este es definitivamente el camino a seguir para mí. Utiliza jQuery, es compacto, corto y expresivo.
nimcap 01 de
2
La respuesta de Subhaze es genial, pero realmente, esta es la correcta.
Molomby
9
Estoy de acuerdo en que esta es una buena solución cuando tiene jQuery 1.5+ pero en el momento de la respuesta la funcionalidad diferida no estaba disponible. :(
subhaze
13

No veo la necesidad de ningún objeto malarky yo mismo. Simple tiene una variable que es un número entero. Cuando inicie una solicitud, incremente el número. Cuando uno complete, decremente. Cuando es cero, no hay solicitudes en curso, así que ya está.

$('#button').click(function() {
    var inProgress = 0;

    function handleBefore() {
        inProgress++;
    };

    function handleComplete() {
        if (!--inProgress) {
            // do what's in here when all requests have completed.
        }
    };

    $.ajax({
        beforeSend: handleBefore,
        complete: function () {
            // whatever
            handleComplete();
            // whatever
        }
    });
    $.ajax({
        beforeSend: handleBefore,
        complete: function () {
            // whatever
            handleComplete();
            // whatever
        }
    });
    $.ajax({
        beforeSend: handleBefore,
        complete: function () {
            // whatever
            handleComplete();
            // whatever
        }
    });
});
Mate
fuente
Esto es muy simple y funciona perfectamente ... ¿Te importaría explicar un poco mejor lo que sucede en if (--inProgress)? Debería controlar si la variable inProgress == 0 y restarle una unidad, pero no obtengo la sintaxis compacta ...
Pitto
1
@Pitto: Vaya ... en realidad, hay un error lógico allí, y debería serlo if (!--inProgress). La versión correcta es análoga a inProgress = inProgress - 1; if (inProgress == 0) { /* do stuff */ }. La forma compacta combina el operador de decremento de prefijo ( --) y el operador de negación (! ) para evaluar a "verdadero" (y, por lo tanto, ejecutar el ifbloque), si la disminución inProgressda como resultado inProgress == 0, y se evalúa como "falso" (y por lo tanto no hace nada) de lo contrario.
Matt
Simplemente maravilloso! Gracias por su tiempo y paciencia.
Pitto
@Pitto: Humm ... ¿puedes vincular el código que estás usando? Se trabaja para mí .
Matt
10

Vale la pena señalar que, dado que $.whenespera que todas las solicitudes de ajax sean argumentos secuenciales (no una matriz), comúnmente se $.whenusarán .apply()así:

// Save all requests in an array of jqXHR objects
var requests = arrayOfThings.map(function(thing) {
    return $.ajax({
        method: 'GET',
        url: 'thing/' + thing.id
    });
});
$.when.apply(this, requests).then(function(resp1, resp2/*, ... */) {
    // Each argument is an array with the following structure: [ data, statusText, jqXHR ]
    var responseArgsArray = Array.prototype.slice.call(this, arguments);

});

Esto se debe a que $.whenacepta argumentos como este

$.when(ajaxRequest1, ajaxRequest2, ajaxRequest3);

Y no así:

$.when([ajaxRequest1, ajaxRequest2, ajaxRequest3]);
Cory Danielson
fuente
Gran respuesta Ahora hay muchas bibliotecas prometedoras, la mayoría de ellas tienen un allmétodo o algo similar, pero me gusta el concepto de mapa. Agradable.
Greg
2
Sí, casi siempre uso $. Cuando al aplicar una serie de solicitudes, y no lo vi en una solución aquí, así que solo lo agregué para tener un buen ejemplo aquí.
Cory Danielson
4

Me gusta la idea de hvgotcodes. Mi sugerencia es agregar un incrementador genérico que compare el número completo con el número necesario y luego ejecute la devolución de llamada final. Esto podría integrarse en la devolución de llamada final.

var sync = {
 callbacksToComplete = 3,
 callbacksCompleted = 0,
 addCallbackInstance = function(){
  this.callbacksCompleted++;
  if(callbacksCompleted == callbacksToComplete) {
   doFinalCallBack();
  }
 }
};

[Editado para reflejar actualizaciones de nombres.]

Adolfo Trudeau
fuente
^ De acuerdo, ayudó a comprender mejor lo que realmente necesitaba.
MisterIsaak
3

EDITAR: tal vez la mejor opción sería crear un punto final de servicio que haga todo lo que hacen las tres solicitudes. De esa manera, solo tiene que hacer una solicitud, y todos los datos están donde los necesita en la respuesta. Si descubres que estás haciendo las mismas 3 solicitudes una y otra vez, probablemente quieras seguir esta ruta. A menudo es una buena decisión de diseño establecer un servicio de fachada en el servidor que agrupe las acciones de servidor más pequeñas que se usan juntas. Solo una idea.


Una forma de hacerlo sería crear un objeto de 'sincronización' en su controlador de clics antes de que llame el ajax. Algo como

var sync = {
   count: 0
}

La sincronización estará vinculada al alcance de las llamadas exitosas automáticamente (cierre). En el controlador de éxito, incrementa el recuento y, si es 3, puede llamar a la otra función.

Alternativamente, podrías hacer algo como

var sync = {
   success1Complete: false,
   ...
   success3Complete: false,
}

cuando se ejecuta cada éxito, cambiaría el valor de la sincronización a verdadero. Tendría que verificar la sincronización para asegurarse de que las tres son verdaderas antes de continuar.

Tenga en cuenta el caso en el que uno de sus xhrs no devuelve el éxito; debe tenerlo en cuenta.

Otra opción sería llamar siempre a la función final en sus controladores de éxito y hacer que acceda a la opción de sincronización para determinar si realmente debe hacer algo. Sin embargo, deberá asegurarse de que la sincronización esté dentro del alcance de esa función.

hvgotcodes
fuente
Me gusta más tu primera sugerencia por un par de razones. Cada solicitud tiene un propósito distinto, por lo que me gustaría mantenerlos separados por ese motivo. Además, las solicitudes están realmente en funciones porque las uso en otro lugar, por lo que estaba tratando de mantener un diseño modular si es posible. ¡Gracias por tu ayuda!
MisterIsaak
@Jisaak, sí, poner un método en el servidor para lidiar con esto podría no tener sentido para ti. Pero es aceptable configurar una fachada en el servidor. Hablas de modularidad, crear un método que maneje las tres cosas es modular.
hvgotcodes
0

Obtuve algunas buenas sugerencias de las respuestas en esta página. Lo adapté un poco para mi uso y pensé que podría compartirlo.

// lets say we have 2 ajax functions that needs to be "synchronized". 
// In other words, we want to know when both are completed.
function foo1(callback) {
    $.ajax({
        url: '/echo/html/',
        success: function(data) {
            alert('foo1');
            callback();               
        }
    });
}

function foo2(callback) {
    $.ajax({
        url: '/echo/html/',
        success: function(data) {
            alert('foo2');
            callback();
        }
    });
}

// here is my simplified solution
ajaxSynchronizer = function() {
    var funcs = [];
    var funcsCompleted = 0;
    var callback;

    this.add = function(f) {
        funcs.push(f);
    }

    this.synchronizer = function() {
        funcsCompleted++;
        if (funcsCompleted == funcs.length) {
            callback.call(this);
        }
    }

    this.callWhenFinished = function(cb) {
        callback = cb;
        for (var i = 0; i < funcs.length; i++) {
            funcs[i].call(this, this.synchronizer);
        }
    }
}

// this is the function that is called when both ajax calls are completed.
afterFunction = function() {
    alert('All done!');
}

// this is how you set it up
var synchronizer = new ajaxSynchronizer();
synchronizer.add(foo1);
synchronizer.add(foo2);
synchronizer.callWhenFinished(afterFunction);

Aquí hay algunas limitaciones, pero para mi caso estuvo bien. También descubrí que para cosas más avanzadas también hay un complemento AOP (para jQuery) que podría ser útil: http://code.google.com/p/jquery-aop/

PålOliver
fuente
0

Encontré este problema hoy, y este fue mi intento ingenuo antes de ver la respuesta aceptada.

<script>
    function main() {
        var a, b, c
        var one = function() {
            if ( a != undefined  && b != undefined && c != undefined ) {
                alert("Ok")
            } else {
                alert( "¬¬ ")
            }
        }

        fakeAjaxCall( function() {
            a = "two"
            one()
        } )
        fakeAjaxCall( function() {
            b = "three"
            one()
        } )
        fakeAjaxCall( function() {
            c = "four"
            one()
        } )
    }
    function fakeAjaxCall( a ) {
        a()
    }
    main()
</script>
OscarRyz
fuente
0

No es jquery (y parece que jquery tiene una solución viable) sino simplemente como otra opción ...

He tenido problemas similares al trabajar mucho con los servicios web de SharePoint: a menudo necesita extraer datos de múltiples fuentes para generar entradas para un solo proceso.

Para resolverlo, incorporé este tipo de funcionalidad en mi biblioteca de abstracción AJAX. Puede definir fácilmente una solicitud que activará un conjunto de controladores cuando se complete. Sin embargo, cada solicitud se puede definir con múltiples llamadas http. Aquí está el componente (y documentación detallada):

DPAJAX en DepressedPress.com

Este simple ejemplo crea una solicitud con tres llamadas y luego pasa esa información, en el orden de las llamadas, a un único controlador:

    // The handler function 
function AddUp(Nums) { alert(Nums[1] + Nums[2] + Nums[3]) }; 

    // Create the pool 
myPool = DP_AJAX.createPool(); 

    // Create the request 
myRequest = DP_AJAX.createRequest(AddUp); 

    // Add the calls to the request 
myRequest.addCall("GET", "http://www.mysite.com/Add.htm", [5,10]); 
myRequest.addCall("GET", "http://www.mysite.com/Add.htm", [4,6]); 
myRequest.addCall("GET", "http://www.mysite.com/Add.htm", [7,13]); 

    // Add the request to the pool 
myPool.addRequest(myRequest); 

Tenga en cuenta que, a diferencia de muchas de las otras soluciones (incluida, creo que la solución "cuándo" en jquery) siempre que este método no fuerce el enhebrado de las llamadas que se realizan, cada una seguirá funcionando tan rápido (o tan lentamente) como lo permita el entorno pero el controlador único solo se llamará cuando todos estén completos. También admite la configuración de valores de tiempo de espera e intentos de reintento si su servicio es un poco inestable.

Lo he encontrado increíblemente útil (e increíblemente simple de entender desde la perspectiva del código). No más encadenamiento, no más conteo de llamadas y ahorro de salida. Solo "configúralo y olvídalo".

Jim Davis
fuente
0

Ok, esto es viejo pero déjame aportar mi solución :)

function sync( callback ){
    syncCount--;
    if ( syncCount < 1 ) callback();
}
function allFinished(){ .............. }

window.syncCount = 2;

$.ajax({
    url: 'url',
    success: function(data) {
        sync( allFinished );
    }
});
someFunctionWithCallback( function(){ sync( allFinished ); } )

Funciona también con funciones que tienen una devolución de llamada. Establece syncCount y llama a la función sync (...) en la devolución de llamada de cada acción.

sic
fuente
0

Encontré una manera más fácil de hacerlo sin la necesidad de métodos adicionales para organizar una cola.

JS

$.ajax({
  type: 'POST',
  url: 'ajax1.php',
  data:{
    id: 1,
    cb:'method1'//declaration of callback method of ajax1.php
  },
  success: function(data){
  //catching up values
  var data = JSON.parse(data);
  var cb=data[0].cb;//here whe catching up the callback 'method1'
  eval(cb+"(JSON.stringify(data));");//here we calling method1 and pass all data
  }
});


$.ajax({
  type: 'POST',
  url: 'ajax2.php',
  data:{
    id: 2,
    cb:'method2'//declaration of callback method of ajax2.php
  },
  success: function(data){
  //catching up values
  var data = JSON.parse(data);
  var cb=data[0].cb;//here whe catching up the callback 'method2'
  eval(cb+"(JSON.stringify(data));");//here we calling method2 and pass all data
  }
});


//the callback methods
function method1(data){
//here we have our data from ajax1.php
alert("method1 called with data="+data);
//doing stuff we would only do in method1
//..
}

function method2(data){
//here we have our data from ajax2.php
alert("method2 called with data="+data);
//doing stuff we would only do in method2
//..
}

PHP (ajax1.php)

<?php
    //catch up callbackmethod
    $cb=$_POST['cb'];//is 'method1'

    $json[] = array(
    "cb" => $cb,
    "value" => "ajax1"
    );      

    //encoding array in JSON format
    echo json_encode($json);
?>

PHP (ajax2.php)

<?php
    //catch up callbackmethod
    $cb=$_POST['cb'];//is 'method2'

    $json[] = array(
    "cb" => $cb,
    "value" => "ajax2"
    );      

    //encoding array in JSON format
    echo json_encode($json);
?>
medicalbird
fuente
-1
async   : false,

Por defecto, todas las solicitudes se envían de forma asincrónica (es decir, esto se establece en verdadero de forma predeterminada). Si necesita solicitudes sincrónicas, configure esta opción en false. Las solicitudes de dominio cruzado y las dataType: "jsonp"solicitudes no admiten operaciones sincrónicas. Tenga en cuenta que las solicitudes sincrónicas pueden bloquear temporalmente el navegador, deshabilitando cualquier acción mientras la solicitud está activa. A partir de jQuery 1.8 , el uso de async: falsecon jqXHR ( $.Deferred) está en desuso; debe utilizar las opciones de devolución de llamada correcta / errónea / completa en lugar de los métodos correspondientes del objeto jqXHR , como por ejemplo, jqXHR.done()o el obsoleto jqXHR.success().

Esteves Klein
fuente
Esta no es una solución adecuada. Nunca debe realizar una solicitud de Ajax sincrónica.
user128216
-2
$.ajax({type:'POST', url:'www.naver.com', dataType:'text', async:false,
    complete:function(xhr, textStatus){},
    error:function(xhr, textStatus){},
    success:function( data ){
        $.ajax({type:'POST', 
            ....
            ....
            success:function(data){
                $.ajax({type:'POST',
                    ....
                    ....
            }
        }
    });

Lo siento, pero no puedo explicar lo que sé porque soy coreano y no puedo hablar ni una palabra en inglés. pero creo que puedes entenderlo fácilmente.

nicecue
fuente
No debería usar ajax de esta manera. Callback Hell ..!
traeper