web-dev-qa-db-fra.com

Ordonner les requêtes ajax

Je trouve que j'ai parfois besoin d'itérer une collection et de faire un appel ajax pour chaque élément. Je veux que chaque appel revienne avant de passer à l'élément suivant afin de ne pas faire exploser le serveur de demandes - ce qui entraîne souvent d'autres problèmes. Et je ne veux pas définir async sur false et geler le navigateur.

Habituellement, cela implique la mise en place d'une sorte de contexte d'itérateur que je franchis à chaque rappel réussi. Je pense qu'il doit y avoir un moyen plus simple et plus propre?

Quelqu'un a-t-il un modèle de conception intelligent pour savoir comment travailler proprement à travers une collection faisant des appels ajax pour chaque article?

62
Scott Evernden

jQuery 1.5+

J'ai développé un plugin $.ajaxQueue() qui utilise le $.Deferred , .queue() , et $.ajax() pour renvoyer également un promesse qui est résolu à la fin de la demande.

/*
* jQuery.ajaxQueue - A queue for ajax requests
* 
* (c) 2011 Corey Frang
* Dual licensed under the MIT and GPL licenses.
*
* Requires jQuery 1.5+
*/ 
(function($) {

// jQuery on an empty object, we are going to use this as our Queue
var ajaxQueue = $({});

$.ajaxQueue = function( ajaxOpts ) {
    var jqXHR,
        dfd = $.Deferred(),
        promise = dfd.promise();

    // queue our ajax request
    ajaxQueue.queue( doRequest );

    // add the abort method
    promise.abort = function( statusText ) {

        // proxy abort to the jqXHR if it is active
        if ( jqXHR ) {
            return jqXHR.abort( statusText );
        }

        // if there wasn't already a jqXHR we need to remove from queue
        var queue = ajaxQueue.queue(),
            index = $.inArray( doRequest, queue );

        if ( index > -1 ) {
            queue.splice( index, 1 );
        }

        // and then reject the deferred
        dfd.rejectWith( ajaxOpts.context || ajaxOpts,
            [ promise, statusText, "" ] );

        return promise;
    };

    // run the actual query
    function doRequest( next ) {
        jqXHR = $.ajax( ajaxOpts )
            .done( dfd.resolve )
            .fail( dfd.reject )
            .then( next, next );
    }

    return promise;
};

})(jQuery);

jQuery 1.4

Si vous utilisez jQuery 1.4, vous pouvez utiliser la file d'attente d'animation sur un objet vide pour créer votre propre "file d'attente" pour vos demandes ajax pour les éléments.

Vous pouvez même en tenir compte dans votre propre remplacement de $.ajax(). Ce plugin $.ajaxQueue() utilise la file d'attente 'fx' standard pour jQuery, qui démarrera automatiquement le premier élément ajouté si la file d'attente n'est pas déjà en cours d'exécution.

(function($) {
  // jQuery on an empty object, we are going to use this as our Queue
  var ajaxQueue = $({});

  $.ajaxQueue = function(ajaxOpts) {
    // hold the original complete function
    var oldComplete = ajaxOpts.complete;

    // queue our ajax request
    ajaxQueue.queue(function(next) {

      // create a complete callback to fire the next event in the queue
      ajaxOpts.complete = function() {
        // fire the original complete if it was there
        if (oldComplete) oldComplete.apply(this, arguments);

        next(); // run the next query in the queue
      };

      // run the query
      $.ajax(ajaxOpts);
    });
  };

})(jQuery);

Exemple d'utilisation

Donc, nous avons un <ul id="items"> Qui a quelques <li> Que nous voulons copier (en utilisant ajax!) Dans le <ul id="output">

// get each item we want to copy
$("#items li").each(function(idx) {

    // queue up an ajax request
    $.ajaxQueue({
        url: '/echo/html/',
        data: {html : "["+idx+"] "+$(this).html()},
        type: 'POST',
        success: function(data) {
            // Write to #output
            $("#output").append($("<li>", { html: data }));
        }
    });
});

démonstration jsfiddle - version 1.4

110
gnarf

Une solution rapide et petite utilisant des promesses différées. Bien que cela utilise le $.Deferred De jQuery, tout autre devrait le faire.

var Queue = function () {
    var previous = new $.Deferred().resolve();

    return function (fn, fail) {
        return previous = previous.then(fn, fail || fn);
    };
};

Utilisation, appelez pour créer de nouvelles files d'attente:

var queue = Queue();

// Queue empty, will start immediately
queue(function () {
    return $.get('/first');
});

// Will begin when the first has finished
queue(function() {
    return $.get('/second');
});

Voir l'exemple avec une comparaison côte à côte des demandes asynchrones.

13
Thomas Nadin

Vous pouvez encapsuler toute cette complexité dans une fonction pour effectuer un appel simple qui ressemble à ceci:

loadSequantially(['/a', '/a/b', 'a/b/c'], function() {alert('all loaded')});

Ci-dessous un croquis (exemple de travail, sauf l'appel ajax). Cela peut être modifié pour utiliser une structure de type file d'attente au lieu d'un tableau

  // load sequentially the given array of URLs and call 'funCallback' when all's done
  function loadSequantially(arrUrls, funCallback) {
     var idx = 0;

     // callback function that is called when individual ajax call is done
     // internally calls next ajax URL in the sequence, or if there aren't any left,
     // calls the final user specified callback function
     var individualLoadCallback = function()   {
        if(++idx >= arrUrls.length) {
           doCallback(arrUrls, funCallback);
        }else {
           loadInternal();
        }
     };

     // makes the ajax call
     var loadInternal = function() {
        if(arrUrls.length > 0)  {
           ajaxCall(arrUrls[idx], individualLoadCallback);
        }else {
           doCallback(arrUrls, funCallback);
        }
     };

     loadInternal();
  };

  // dummy function replace with actual ajax call
  function ajaxCall(url, funCallBack) {
     alert(url)
     funCallBack();
  };

  // final callback when everything's loaded
  function doCallback(arrUrls, func)   {
     try   {
        func();
     }catch(err) {
        // handle errors
     }
  };
3
naikus

Idéalement, une coroutine avec plusieurs points d'entrée afin que chaque rappel du serveur puisse appeler la même coroutine sera soignée. Merde, cela est sur le point d'être implémenté dans Javascript 1.7.

Permettez-moi d'essayer d'utiliser la fermeture ...

function BlockingAjaxCall (URL,arr,AjaxCall,OriginalCallBack)
{    
     var nextindex = function()
     {
         var i =0;
         return function()
         {
             return i++;
         }
     };

     var AjaxCallRecursive = function(){
             var currentindex = nextindex();
             AjaxCall
             (
                 URL,
                 arr[currentindex],
                 function()
                 {
                     OriginalCallBack();
                     if (currentindex < arr.length)
                     {
                         AjaxCallRecursive();
                     }
                 }
             );
     };
     AjaxCallRecursive();    
}
// suppose you always call Ajax like AjaxCall(URL,element,callback) you will do it this way
BlockingAjaxCall(URL,myArray,AjaxCall,CallBack);
3
DonnieKun

Oui, alors que les autres réponses fonctionneront, elles sont beaucoup de code et ont l'air désordonnées. Frame.js a été conçu pour répondre avec élégance à cette situation. https://github.com/bishopZ/Frame.js

Par exemple, cela entraînera le blocage de la plupart des navigateurs:

for(var i=0; i<1000; i++){
    $.ajax('myserver.api', { data:i, type:'post' });
}

Bien que cela ne:

for(var i=0; i<1000; i++){
    Frame(function(callback){
        $.ajax('myserver.api', { data:i, type:'post', complete:callback });
    });
}
Frame.start();

En outre, l'utilisation de Frame vous permet de mettre en cascade les objets de réponse et de les traiter tous une fois la série complète de AJAX requête terminée (si vous le souhaitez):

var listOfAjaxObjects = [ {}, {}, ... ]; // an array of objects for $.ajax
$.each(listOfAjaxObjects, function(i, item){
    Frame(function(nextFrame){ 
        item.complete = function(response){
            // do stuff with this response or wait until end
            nextFrame(response); // ajax response objects will waterfall to the next Frame()
        $.ajax(item);
    });
});
Frame(function(callback){ // runs after all the AJAX requests have returned
    var ajaxResponses = [];
    $.each(arguments, function(i, arg){
        if(i!==0){ // the first argument is always the callback function
            ajaxResponses.Push(arg);
        }
    });
    // do stuff with the responses from your AJAX requests
    // if an AJAX request returned an error, the error object will be present in place of the response object
    callback();
});
Frame.start()
2
BishopZ

Je poste cette réponse en pensant qu'elle pourrait aider d'autres personnes à l'avenir, à la recherche de solutions simples dans le même scénario.

Cela est désormais possible en utilisant également le support de promesse natif introduit dans ES6. Vous pouvez encapsuler l'appel ajax dans une promesse et le renvoyer au gestionnaire de l'élément.

function ajaxPromise(elInfo) {
    return new Promise(function (resolve, reject) {
        //Do anything as desired with the elInfo passed as parameter

        $.ajax({
            type: "POST",
            url: '/someurl/',
            data: {data: "somedata" + elInfo},
            success: function (data) {
                //Do anything as desired with the data received from the server,
                //and then resolve the promise
                resolve();
            },
            error: function (err) {
                reject(err);
            },
            async: true
        });

    });
}

Appelez maintenant la fonction récursivement, d'où vous avez la collection des éléments.

function callAjaxSynchronous(elCollection) {
    if (elCollection.length > 0) {
        var el = elCollection.shift();
        ajaxPromise(el)
        .then(function () {
            callAjaxSynchronous(elCollection);
        })
        .catch(function (err) {
            //Abort further ajax calls/continue with the rest
            //callAjaxSynchronous(elCollection);
        });
    }
    else {
        return false;
    }
}
2
Sandip Ghosh

J'utilise http://developer.yahoo.com/yui/3/io/#queue pour obtenir cette fonctionnalité.

Comme vous le dites, la seule solution que je peux trouver est de maintenir une liste des appels/rappels en attente. Ou imbriquer le prochain appel dans le rappel précédent, mais cela semble un peu désordonné.

1
unomi

Vous pouvez réaliser la même chose en utilisant then.

var files = [
  'example.txt',
  'example2.txt',
  'example.txt',
  'example2.txt',
  'example.txt',
  'example2.txt',
  'example2.txt',
  'example.txt'
];

nextFile().done(function(){
  console.log("done",arguments)
});

function nextFile(text){
  var file = files.shift();
  if(text)
    $('body').append(text + '<br/>');
  if(file)
    return $.get(file).then(nextFile);
}

http://plnkr.co/edit/meHQHU48zLTZZHMCtIHm?p=preview

1
Shanimal

Je suggérerais une approche un peu plus sophistiquée qui est réutilisable pour différents cas.
Je l'utilise par exemple lorsque j'ai besoin de ralentir une séquence d'appels lorsque l'utilisateur tape dans l'éditeur de texte.

Mais je suis sûr que cela devrait également fonctionner lors de l'itération dans la collection. Dans ce cas, il peut mettre les demandes en file d'attente et envoyer un seul appel AJAX au lieu de 12.

queueing = {
    callTimeout:                 undefined,
    callTimeoutDelayTime:        1000,
    callTimeoutMaxQueueSize:     12,
    callTimeoutCurrentQueueSize: 0,

    queueCall: function (theCall) {
        clearTimeout(this.callTimeout);

        if (this.callTimeoutCurrentQueueSize >= this.callTimeoutMaxQueueSize) {
            theCall();
            this.callTimeoutCurrentQueueSize = 0;
        } else {
            var _self = this;

            this.callTimeout = setTimeout(function () {
                theCall();
                _self.callTimeoutCurrentQueueSize = 0;
            }, this.callTimeoutDelayTime);
        }

        this.callTimeoutCurrentQueueSize++;
    }
}
1
Oleg Lazaryev