Последовательность запросов AJAX

Я нахожу, что иногда мне нужно перебрать некоторую коллекцию и сделать вызов ajax для каждого элемента. Я хочу, чтобы каждый вызов возвращался до перехода к следующему элементу, чтобы я не загружал сервер запросами, что часто приводит к другим проблемам. И я не хочу устанавливать асинхронный режим в ложное и заморозить браузер.

Обычно это включает в себя настройку некоторого контекста итератора, который я выполняю при каждом обратном вызове. Я думаю, что должен быть чище проще?

Есть ли у кого-нибудь умный шаблон дизайна для того, как аккуратно работать с коллекцией, делая ajax-вызовы для каждого элемента?

10 ответов

Решение

jQuery 1.5+

Я разработал $.ajaxQueue() плагин, который использует $.Deferred, .queue(), а также $.ajax() также вернуть обещание, которое разрешается после завершения запроса.

/*
* 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

Если вы используете jQuery 1.4, вы можете использовать очередь анимации для пустого объекта, чтобы создать свою собственную "очередь" для ваших запросов ajax об элементах.

Вы даже можете учесть это в своем собственном $.ajax() замена. Этот плагин $.ajaxQueue() использует стандартную очередь 'fx' для jQuery, которая автоматически запустит первый добавленный элемент, если очередь еще не запущена.

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

Пример использования

Итак, у нас есть <ul id="items"> который имеет некоторые <li> что мы хотим скопировать (используя ajax!) в <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 }));
        }
    });
});

демонстрация jsfiddle - версия 1.4

Быстрое и маленькое решение с использованием отложенных обещаний. Хотя это использует JQuery $.Deferred, любой другой должен сделать.

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

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

Использование, позвоните, чтобы создать новые очереди:

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

Смотрите пример с параллельным сравнением асинхронных запросов.

В идеале сопрограмма с несколькими точками входа, поэтому каждый обратный вызов с сервера может вызывать одну и ту же сопрограмму, должна быть аккуратной. Черт, это должно быть реализовано в Javascript 1.7.

Позвольте мне попробовать использовать закрытие...

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

Вы можете обернуть всю эту сложность в функцию, чтобы сделать простой вызов, который выглядит следующим образом:

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

Ниже приведен примерный набросок (рабочий пример, кроме вызова ajax). Это может быть изменено, чтобы использовать подобную очереди структуру вместо массива

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

Да, в то время как другие ответы будут работать, они много кода и выглядят грязно. Frame.js был разработан для элегантного решения этой ситуации. https://github.com/bishopZ/Frame.js

Например, это приведет к зависанию большинства браузеров:

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

Пока этого не будет

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

Кроме того, использование Frame позволяет вам создавать объекты-ответчики и обрабатывать их все после завершения всей серии запросов AJAX (если вы хотите):

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()

Я публикую этот ответ, думая, что он может помочь другим людям в будущем, ища несколько простых решений в том же сценарии.

Теперь это возможно также с помощью встроенной поддержки обещаний, представленной в ES6. Вы можете заключить вызов ajax в обещание и вернуть его обработчику элемента.

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

    });
}

Теперь вызовите функцию рекурсивно, откуда у вас есть коллекция элементов.

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

Я использую http://developer.yahoo.com/yui/3/io/ чтобы получить эту функциональность.

Единственное решение, которое я могу предложить, это, как вы говорите, ведение списка ожидающих вызовов / обратных вызовов. Или вложив следующий вызов в предыдущий обратный вызов, но это выглядит немного грязно.

Я бы предложил немного более сложный подход, который можно использовать в разных случаях.
Я использую его, например, когда мне нужно замедлить последовательность вызовов, когда пользователь печатает в текстовом редакторе.

Но я уверен, что это должно работать и при переборе коллекции. В этом случае он может ставить запросы в очередь и может отправлять один вызов AJAX вместо 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++;
    }
}

Вы можете достичь того же, используя 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

Есть очень простой способ добиться этого, добавив async: false в качестве свойства к вызову ajax. Это позволит убедиться, что вызов ajax завершен, прежде чем анализировать остальную часть кода. Я успешно использовал это в циклах много раз.

Например.

      $.ajax({
    url: "",
    type: "GET",
    async: false
...
Другие вопросы по тегам