retorno de chamada jQuery para várias chamadas ajax

132

Quero fazer três chamadas ajax em um evento de clique. Cada chamada ajax faz uma operação distinta e retorna os dados necessários para um retorno de chamada final. As chamadas em si não são dependentes uma da outra, todas elas podem ir ao mesmo tempo; no entanto, eu gostaria de ter um retorno de chamada final quando todas as três estiverem concluídas.

$('#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*/});
});
MisterIsaak
fonte

Respostas:

112

Aqui está um objeto de retorno de chamada que escrevi, no qual você pode definir um único retorno de chamada para disparar uma vez concluído, ou permitir que cada um tenha seu próprio retorno de chamada e acioná-los todos quando terminar:

AVISO PRÉVIO

Desde o jQuery 1.5+, você pode usar o método adiado, conforme descrito em outra resposta:

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

Exemplo de diferido aqui

para jQuery <1.5, o seguinte funcionará ou se você precisar que suas chamadas ajax sejam acionadas em horários desconhecidos, como mostrado aqui com dois botões: acionado após o clique de ambos os botões

[uso]

para retorno de chamada único depois de concluído: Exemplo de Trabalho

// 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 um com seu próprio retorno de chamada quando tudo estiver concluído: Exemplo de Trabalho

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

[O 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
fonte
1
Obrigado pelo ótimo exemplo de código! Acho que vou conseguir tropeçar no resto. Obrigado novamente!
MisterIsaak
Sem problemas, feliz que ajudou! Eu fiquei meio empolgado com isso: P ainda tem mais algumas idéias para isso. Pretendo atualizá-lo para onde você não precisa especificar um número de solicitações na inicialização.
Subhaze
Bom, espero que sim! Muito interessante, eu sugiro jogar a opção de adicionar um retorno de chamada adicional. Eu fiz e funciona perfeito para o que eu quero fazer. Obrigado novamente!
MisterIsaak
Esqueci-me de adicioná-lo ao caso de uso: P use setCallbackpara adicionar mais à fila. Embora, agora que penso nisso ... Eu deveria chamá-lo addCallbackou algo do tipo ... e não apenas definir uma vez que está adicionando para a fila
subhaze
A singleCallbackvariável na 2ª linha é desnecessária? Ou eu estou esquecendo de alguma coisa?
Nimcap #
158

Parece que você tem algumas respostas para isso, no entanto, acho que há algo que vale a pena mencionar aqui que simplificará bastante o seu código. O jQuery introduziu o $.whenna v1.5. Parece que:

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

Não o vi mencionado aqui, espero que ajude.

Greg
fonte
6
Obrigado pela resposta, este é definitivamente o caminho a percorrer para mim. usa jQuery, é compacto, curto e expressivo.
Nimcap #
2
A resposta do Subhaze é ótima, mas, na verdade, essa é a correta.
Molomby 6/09/12
9
Concordo que esta é uma boa solução quando você possui o jQuery 1.5+, mas no momento da resposta a funcionalidade adiada não estava disponível. :(
subhaze
13

Não vendo a necessidade de nenhum objeto mal-intencionado. Simples tem uma variável que é um número inteiro. Quando você inicia uma solicitação, aumente o número. Quando alguém terminar, diminua-o. Quando é zero, não há solicitações em andamento, então você está pronto.

$('#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
        }
    });
});
Matt
fonte
Isso é muito simples e funciona perfeitamente ... Você se importaria de explicar um pouco melhor o que acontece em if (- inProgress)? Deve controlar se InProgress variável == 0 e subtrair uma unidade para isso, mas eu não entendo a sintaxe compacta ...
Pitto
1
@ Pitto: Opa ... na verdade, há um erro de lógica lá, e deveria ser if (!--inProgress). A versão correta é análoga a inProgress = inProgress - 1; if (inProgress == 0) { /* do stuff */ }. A forma compacta combina o operador de decremento-- do prefixo ( ) e o operador de negação ( !) para avaliar como "true" (e, portanto, executar o ifbloco), se a decrementar inProgressresultar em inProgress == 0, e avalia como "false" (e, portanto, não faz nada) caso contrário.
Matt
Simplesmente maravilhoso! Obrigado pelo seu tempo e paciência.
Pitto
@ Pitto: Humm .. você pode criar um link para o código que está usando? Isso funciona para mim .
Matt
10

É importante notar que desde que $.whenespera que todos os pedidos de ajax como argumentos seqüenciais (não um array) você vai comumente ver $.whenusado com .apply()assim:

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

});

Isso ocorre porque $.whenaceita argumentos como este

$.when(ajaxRequest1, ajaxRequest2, ajaxRequest3);

E não é assim:

$.when([ajaxRequest1, ajaxRequest2, ajaxRequest3]);
Cory Danielson
fonte
Ótima resposta. Agora existem muitas bibliotecas de boas promessas, a maioria delas possui um allmétodo ou algo semelhante, mas eu gosto do conceito de mapa. Agradável.
Greg
2
Sim, eu quase sempre uso $ .when aplicando uma variedade de solicitações a ele, e não o localizei em uma solução aqui, então apenas o adicionei para ter um bom exemplo aqui.
Cory Danielson
4

Eu gosto da ideia dos hvgotcodes. Minha sugestão é adicionar um incrementador genérico que compare o número completo com o número necessário e execute o retorno de chamada final. Isso pode ser incorporado no retorno de chamada final.

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

[Editado para refletir as atualizações de nome.]

Adolph Trudeau
fonte
^ Concordo, ajudou a entender melhor o que eu realmente precisava.
MisterIsaak
3

EDIT - talvez a melhor opção seja criar um terminal em serviço que faça tudo o que as três solicitações fazem. Dessa forma, você só precisa fazer uma solicitação e todos os dados estão onde você precisa que eles estejam na resposta. Se você achar que está fazendo os mesmos três pedidos repetidamente, provavelmente desejará seguir esse caminho. Geralmente, é uma boa decisão de design configurar um serviço de fachada no servidor que agrupa geralmente ações menores do servidor. Apenas uma ideia.


Uma maneira de fazer isso seria criar um objeto 'sync' no seu manipulador de cliques antes que o ajax chame. Algo como

var sync = {
   count: 0
}

A sincronização será vinculada ao escopo das chamadas de sucesso automaticamente (fechamento). No manipulador de sucesso, você incrementa a contagem e, se for 3, pode chamar a outra função.

Como alternativa, você poderia fazer algo como

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

quando cada sucesso é executado, ele altera o valor na sincronização para true. Você precisaria verificar a sincronização para garantir que todos os três sejam verdadeiros antes de continuar.

Observe o caso em que um dos seus xhrs não retorna com êxito - você precisa dar conta disso.

Outra opção seria sempre chamar a função final em seus manipuladores de sucesso e acessar a opção de sincronização para determinar se realmente deve fazer alguma coisa. Você precisaria garantir que a sincronização esteja no escopo dessa função.

hvgotcodes
fonte
Eu gosto mais da sua primeira sugestão por algumas razões. Cada solicitação tem uma finalidade distinta, portanto, gostaria de mantê-las separadas por esse motivo. Além disso, os pedidos estão realmente em funções porque eu os uso em outros lugares, então eu estava tentando manter um design modular, se possível. Obrigado pela ajuda!
MisterIsaak
@ Jisaak, sim, colocar um método no servidor para lidar com isso pode não fazer sentido para você. Mas é aceitável configurar uma fachada no servidor. Você fala sobre modularidade, criando um método que lida com as três coisas é modular.
hvgotcodes
0

Recebi boas dicas das respostas desta página. Eu o adaptei um pouco para o meu uso e pensei em compartilhar.

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

Existem algumas limitações aqui, mas para o meu caso, tudo bem. Também descobri que, para coisas mais avançadas, também existe um plug-in AOP (para jQuery) que pode ser útil: http://code.google.com/p/jquery-aop/

PålOliver
fonte
0

Hoje me deparei com esse problema e essa foi minha tentativa ingênua antes de assistir à resposta aceita.

<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
fonte
0

Não é jquery (e parece que o jquery tem uma solução viável), mas apenas como outra opção ....

Eu tive problemas semelhantes ao trabalhar muito com os serviços Web do SharePoint - muitas vezes você precisa extrair dados de várias fontes para gerar entrada para um único processo.

Para resolvê-lo, integrei esse tipo de funcionalidade na minha biblioteca de abstração AJAX. Você pode definir facilmente uma solicitação que acionará um conjunto de manipuladores ao concluir. No entanto, cada solicitação pode ser definida com várias chamadas http. Aqui está o componente (e documentação detalhada):

DPAJAX em DepressedPress.com

Este exemplo simples cria uma solicitação com três chamadas e passa essas informações, na ordem de chamada, para um único manipulador:

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

Observe que, diferentemente de muitas outras soluções (inclusive, acredito que a solução "quando" no jquery), desde que esse método não force o encadeamento único das chamadas feitas - cada uma ainda será executada tão rapidamente (ou tão lentamente) quanto o ambiente permitir mas o manipulador único será chamado somente quando todos estiverem concluídos. Ele também suporta a configuração de valores de tempo limite e novas tentativas se o seu serviço for um pouco ruim.

Eu achei incrivelmente útil (e incrivelmente simples de entender da perspectiva do código). Chega de encadeamento, contagem de chamadas e economia de saída. Basta configurá-lo e esquecê-lo".

Jim Davis
fonte
0

Ok, isso é antigo, mas deixe-me contribuir com minha solução :)

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

Também funciona com funções que possuem retorno de chamada. Você define o syncCount e chama a função sync (...) no retorno de chamada de cada ação.

sic
fonte
0

Encontrei uma maneira mais fácil de fazer isso sem a necessidade de métodos extras que organizam uma fila.

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
fonte
-1
async   : false,

Por padrão, todas as solicitações são enviadas de forma assíncrona (ou seja, isso é definido como verdadeiro por padrão). Se você precisar de solicitações síncronas, configure esta opção para false. Solicitações entre domínios e dataType: "jsonp"solicitações não suportam operação síncrona. Observe que solicitações síncronas podem bloquear temporariamente o navegador, desativando qualquer ação enquanto a solicitação estiver ativa. A partir do jQuery 1.8 , o uso de async: falsecom jqXHR ( $.Deferred) está obsoleto; você deve usar as opções de retorno de chamada com êxito / erro / concluir , em vez dos métodos correspondentes do objeto jqXHR , como jqXHR.done()o obsoleto jqXHR.success().

Esteves Klein
fonte
Esta não é uma solução adequada. Você nunca deve fazer uma solicitação síncrona do Ajax.
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',
                    ....
                    ....
            }
        }
    });

Me desculpe, mas não posso explicar o que escrevo porque sou coreano que não consegue nem falar uma palavra em inglês. mas acho que você pode entender facilmente.

Belo palpite
fonte
Não deve usar ajax dessa maneira. Callback Inferno ..!
traeper