Параллельные асинхронные запросы Ajax с использованием jQuery

Я хотел бы обновить страницу на основе результатов нескольких запросов ajax/json. Используя jQuery, я могу "связать" обратные вызовы, как в этом очень простом урезанном примере:

$.getJSON("/values/1", function(data) {
  // data = {value: 1}
  var value_1 = data.value;

  $.getJSON("/values/2", function(data) {
    // data = {value: 42}
    var value_2 = data.value;

    var sum = value_1 + value_2;

    $('#mynode').html(sum);
  });

});

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

14 ответов

Решение

Попробуйте это решение, которое может поддерживать любое конкретное количество параллельных запросов:

var done = 4; // number of total requests
var sum = 0;

/* Normal loops don't create a new scope */
$([1,2,3,4,5]).each(function() {
  var number = this;
  $.getJSON("/values/" + number, function(data) {
    sum += data.value;
    done -= 1;
    if(done == 0) $("#mynode").html(sum);
  });
});

jQuery $.when () и $.done() - это именно то, что вам нужно:

$.when($.ajax("/page1.php"), $.ajax("/page2.php"))
  .then(myFunc, myFailure);

Выполнить несколько запросов AJAX параллельно

При работе с API иногда требуется выполнить несколько запросов AJAX для разных конечных точек. Вместо того чтобы ждать выполнения одного запроса, прежде чем выдать следующий, вы можете ускорить процесс с помощью jQuery, запросив данные параллельно, используя jQuery. $.when() функция:

JS

$.when($.get('1.json'), $.get('2.json')).then(function(r1, r2){
   console.log(r1[0].message + " " + r2[0].message);
});

Функция обратного вызова выполняется, когда оба этих запроса GET завершаются успешно. $.when() принимает обещания, возвращенные двумя вызовами $.get(), и создает новый объект обещания. Аргументы обратного вызова r1 и r2 - это массивы, первые элементы которых содержат ответы сервера.

Обновление: согласно ответу Яира Левиеля, этот ответ устарел. Используйте библиотеку обещаний, такую ​​как jQuery.when() или Q.js.


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

Примечание: я бы использовал расширения Rx для JavaScript вместо этого, если бы думал, что мой клиент будет в порядке, если взять зависимость от еще одной другой сторонней библиотеки:)

// jQuery extension for running multiple async methods in parallel
// and getting a callback with all results when all of them have completed.
//
// Each worker is a function that takes a callback as its only argument, and
// fires up an async process that calls this callback with its result.
//
// Example:
//      $.parallel(
//          function (callback) { $.get("form.htm", {}, callback, "html"); },
//          function (callback) { $.post("data.aspx", {}, callback, "json"); },
//          function (formHtml, dataJson) { 
//              // Handle success; each argument to this function is 
//              // the result of correlating ajax call above.
//          }
//      );

(function ($) {

    $.parallel = function (anyNumberOfWorkers, allDoneCallback) {

    var workers = [];
    var workersCompleteCallback = null;

    // To support any number of workers, use "arguments" variable to
    // access function arguments rather than the names above.
    var lastArgIndex = arguments.length - 1;
    $.each(arguments, function (index) {
        if (index == lastArgIndex) {
            workersCompleteCallback = this;
        } else {
            workers.push({ fn: this, done: false, result: null });
        }
    });

    // Short circuit this edge case
    if (workers.length == 0) {
        workersCompleteCallback();
        return;
    }

    // Fire off each worker process, asking it to report back to onWorkerDone.
    $.each(workers, function (workerIndex) {
        var worker = this;
        var callback = function () { onWorkerDone(worker, arguments); };
        worker.fn(callback);
    });

    // Store results and update status as each item completes.
    // The [0] on workerResultS below assumes the client only needs the first parameter
    // passed into the return callback. This simplifies the handling in allDoneCallback,
    // but may need to be removed if you need access to all parameters of the result.
    // For example, $.post calls back with success(data, textStatus, XMLHttpRequest).  If
    // you need textStatus or XMLHttpRequest then pull off the [0] below.
    function onWorkerDone(worker, workerResult) {
        worker.done = true;
        worker.result = workerResult[0]; // this is the [0] ref'd above.
        var allResults = [];
        for (var i = 0; i < workers.length; i++) {
            if (!workers[i].done) return;
            else allResults.push(workers[i].result);
        }
        workersCompleteCallback.apply(this, allResults);
    }
};

})(jQuery);

Вот моя попытка напрямую ответить на ваш вопрос

По сути, вы просто создаете и вызываете AJAX-стек, выполняете их все, и при завершении всех событий вызывается предоставленная функция - предоставленный аргумент представляет собой массив результатов всех предоставленных ajax-запросов.

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

<script type="text/javascript" src="http://jqueryjs.googlecode.com/files/jquery-1.3.2.min.js"></script>
<script type="text/javascript">

var ParallelAjaxExecuter = function( onComplete )
{
  this.requests = [];
  this.results = [];
  this.onComplete = onComplete; 
}

ParallelAjaxExecuter.prototype.addRequest = function( method, url, data, format )
{
  this.requests.push( {
      "method"    : method
    , "url"       : url
    , "data"      : data
    , "format"    : format
    , "completed" : false
  } )
}

ParallelAjaxExecuter.prototype.dispatchAll = function()
{
  var self = this;
  $.each( self.requests, function( i, request )
    {
    request.method( request.url, request.data, function( r )
    {
      return function( data )
      {
        console.log
        r.completed = true;
        self.results.push( data );
        self.checkAndComplete();
      }
    }( request ) )
  } )
}

ParallelAjaxExecuter.prototype.allRequestsCompleted = function()
{
  var i = 0;
  while ( request = this.requests[i++] )
  {
    if ( request.completed === false )
    {
      return false;
    }
  }
  return true;
},

ParallelAjaxExecuter.prototype.checkAndComplete = function()
{
  if ( this.allRequestsCompleted() )
  {
    this.onComplete( this.results );
  }
}

var pe = new ParallelAjaxExecuter( function( results )
{
  alert( eval( results.join( '+' ) ) );
} );

pe.addRequest( $.get, 'test.php', {n:1}, 'text' );
pe.addRequest( $.get, 'test.php', {n:2}, 'text' );
pe.addRequest( $.get, 'test.php', {n:3}, 'text' );
pe.addRequest( $.get, 'test.php', {n:4}, 'text' );

pe.dispatchAll();

</script>

вот test.php

<?php

echo pow( $_GET['n'], 2 );

?>

ОБНОВЛЕНИЕ И еще два года спустя, это выглядит безумно, потому что принятый ответ изменился на что-то намного лучшее! (Хотя все еще не так хорошо, как ответ Яир Левиэль, используя JQuery when)

18 месяцев спустя я просто ударил что-то подобное. У меня есть кнопка обновления, и я хочу, чтобы старый контент fadeOut а затем новый контент fadeIn, Но мне тоже нужно get новый контент. fadeOut и get являются асинхронными, но было бы пустой тратой времени на их последовательный запуск.

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

var parallel = function(actions, finished) {

  finishedCount = 0;
  var results = [];

  $.each(actions, function(i, action) {

    action(function(result) {

      results[i] = result;
      finishedCount++;

      if (finishedCount == actions.length) {
        finished(results);
      }
    });
  });
};

Вы передаете ему массив функций для параллельного запуска. Каждая функция должна принимать другую функцию, которой она передает свой результат (если есть). parallel будет выполнять эту функцию.

Вы также передаете ему функцию, которая будет вызвана после завершения всех операций. Это получит массив со всеми результатами в. Таким образом, мой пример был:

refreshButton.click(function() {

  parallel([
       function(f) { 
         contentDiv.fadeOut(f); 
       },
       function(f) { 
         portlet.content(f); 
       },
     ], 
     function(results) {
      contentDiv.children().remove();
      contentDiv.append(results[1]);
      contentDiv.fadeIn();
  });
});

Поэтому, когда моя кнопка обновления нажата, я запускаю jQuery's fadeOut эффект, а также мой собственный portlet.content функция (которая делает асинхронный get, создает новый бит контента и передает его), а затем, когда оба завершены, я удаляю старый контент, добавляю результат второй функции (которая находится в results[1]) а также fadeIn новый контент.

Как fadeOut ничего не передает в функцию завершения, results[0] предположительно содержит undefinedтак что я игнорирую это. Но если бы у вас было три операции с полезными результатами, они бы каждый слот в results массив, в том же порядке вы передали функции.

Вы могли бы сделать что-то вроде этого

var allData = []
$.getJSON("/values/1", function(data) {
    allData.push(data);
    if(data.length == 2){
      processData(allData) // where process data processes all the data
    }
});

$.getJSON("/values/2", function(data) {
    allData.push(data);
    if(data.length == 2){
        processData(allData) // where process data processes all the data
    }
});

var processData = function(data){
     var sum = data[0] + data[1]
     $('#mynode').html(sum);
}

Со следующим расширением JQuery (to может быть записано как отдельная функция, вы можете сделать это:

$.whenAll({
    val1: $.getJSON('/values/1'),
    val2: $.getJSON('/values/2')
})
    .done(function (results) {
        var sum = results.val1.value + results.val2.value;

        $('#mynode').html(sum);
    });

Расширение JQuery (1.x) whenAll():

$.whenAll = function (deferreds) {
    function isPromise(fn) {
        return fn && typeof fn.then === 'function' &&
            String($.Deferred().then) === String(fn.then);
    }
    var d = $.Deferred(),
        keys = Object.keys(deferreds),
        args = keys.map(function (k) {
            return $.Deferred(function (d) {
                var fn = deferreds[k];

                (isPromise(fn) ? fn : $.Deferred(fn))
                    .done(d.resolve)
                    .fail(function (err) { d.reject(err, k); })
                ;
            });
        });

    $.when.apply(this, args)
        .done(function () {
            var resObj = {},
                resArgs = Array.prototype.slice.call(arguments);
            resArgs.forEach(function (v, i) { resObj[keys[i]] = v; });
            d.resolve(resObj);
        })
        .fail(d.reject);

    return d;
};

Смотрите пример jsbin: http://jsbin.com/nuxuciwabu/edit?js,console

Наиболее профессиональным решением для меня было бы использование async.js и Array.reduce следующим образом:

        async.map([1, 2, 3, 4, 5], function (number, callback) {
            $.getJSON("/values/" + number, function (data) {
                callback(null, data.value);
            });
        }, function (err, results) {
            $("#mynode").html(results.reduce(function(previousValue, currentValue) {
                return previousValue + currentValue;
            }));
        });

Вот реализация, использующая mbostock / queue:

queue()
  .defer(function(callback) {
    $.post('/echo/json/', {json: JSON.stringify({value: 1}), delay: 1}, function(data) {
      callback(null, data.value);
    });
  })
  .defer(function(callback) {
    $.post('/echo/json/', {json: JSON.stringify({value: 3}), delay: 2}, function(data) {
      callback(null, data.value);
    });
  })
  .awaitAll(function(err, results) {
    var result = results.reduce(function(acc, value) {
      return acc + value;
    }, 0);
    console.log(result);
  });

Связанная скрипка: http://jsfiddle.net/MdbW2/

Если результат одного запроса зависит от другого, вы не можете сделать их параллельными.

Опираясь на ответ Яир. Вы можете определить ajax-обещания динамически.

var start = 1; // starting value
var len = 2; // no. of requests

var promises = (new Array(len)).fill().map(function() {
    return $.ajax("/values/" + i++);
});

$.when.apply($, promises)
  .then(myFunc, myFailure);

Мне нужно было несколько параллельных вызовов ajax и jquery$.whenсинтаксис не поддавался в полной мере$.ajaxформат, с которым я привык работать. Поэтому я только что создалsetIntervalтаймер для периодической проверки возврата каждого из вызовов ajax. Как только они все будут возвращены, я смогу продолжить.

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

Предположим, у вас есть массив имен файлов.

var templateNameArray=["test.html","test2.html","test3.html"];

htmlTemplatesLoadStateMap={};
var deffereds=[];
  for (var i = 0; i < templateNameArray.length; i++)
       {
        if (!htmlTemplatesLoadStateMap[templateNameArray[i]]) 
            {         
              deferreds.push($.get("./Content/templates/" +templateNameArray[i], 

                  function (response, status, xhr) {
                      if (status == "error") { } 
                        else {
                                $("body").append(response);
                               }
                         }));             
htmlTemplatesLoadStateMap[templateNameArray[i]] = true;
                       }
                  }
                                      $.when.all(deferreds).always(function(resultsArray) {   yourfunctionTobeExecuted(yourPayload);
                                });
Другие вопросы по тегам