Динамический массив jQuery Deferred/Promises не выполняет обратные вызовы в правильном порядке

Благодарен за любое понимание того, что я здесь неправильно понимаю. Мое требование заключается в следующем:

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

Вариант 1:

for (var i = 0; i < myUrlArray.length; i++) {
    $.ajax({
        url: myUrlArray[i]
    }).done(function(response) {
        // Do something with response
    });
}

Очевидно, что это не работает, так как нет гарантии, что ответы будут выполнены в правильном порядке.

Вариант 2:

var promises = [];
for (var i = 0; i < myUrlArray.length; i++) {
    promises.push($.ajax({
        url: myUrlArray[i]
    }));
}
$.when.apply($, promises).then(function() {
    // Do something with each response
});

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

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

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

Моя попытка:

var promises = [];
for (var i = 0; i < myUrlArray.length; i++) {
    promises.push($.ajax({
        url: myUrlArray[i] // Add each AJAX Deferred to the promises array
    }));
}
(function handleAJAX() {
    var promise;
    if (promises.length) {
        promise = promises.shift(); // Grab the first one in the stack
        promise.then(function(response) { // Set up 'done' callback
            // Do something with response

            if (promises.length) {
                handleAJAX(); // Move onto the next one
            }
        });
    }
}());

Проблема в том, что обратные вызовы выполняются в совершенно случайном порядке! Например, если я добавлю в массив «home.html», «page2.html», «page3.html», порядок ответов не обязательно будет «home.html», «page2.html», «page3». .html».

Я, очевидно, в корне неправильно понимаю что-то о том, как работают обещания. Любая помощь с благодарностью!

Ваше здоровье

ИЗМЕНИТЬ

Ладно, теперь я еще больше запутался. Я сделал это JSFiddle с одним массивом, используя ответ Alnitak и другой, использующий ответ JoeFletch, и ни один из них не работает так, как я бы ожидать! Кто-нибудь может увидеть, что здесь происходит?

ИЗМЕНИТЬ 2

Получил работу! Основываясь на приведенном ниже ответе JoeFletch, я адаптировал решение следующим образом:

var i, responseArr = [];

for (i = 0; i < myUrlArray.length; i++) {
    responseArr.push('0'); // <-- Add 'unprocessed' flag for each pending request
    (function(ii) {
        $.ajax({
            url: myUrlArray[ii]
        }).done(function(response) {
            responseArr[ii] = response; // <-- Store response in array
        }).fail(function(xhr, status, error) {
            responseArr[ii] = 'ERROR';
        }).always(function(response) {
            for (var iii = 0; iii < responseArr.length; iii++) { // <-- Loop through entire response array from the beginning
                if (responseArr[iii] === '0') {
                    return; // As soon as we hit an 'unprocessed' request, exit loop
                }
                else if (responseArr[iii] !== 'done') {
                    $('#target').append(responseArr[iii]); // <-- Do actual callback DOM append stuff
                    responseArr[iii] = 'done'; // <-- Set 'complete' flag for this request
                }
            }
        });
    }(i)); // <-- pass current value of i into closure to encapsulate
}

TL;DR: я не понимаю промисов jQuery, я заработал без них. :)


person chrisfrancis27    schedule 08.11.2012    source источник
comment
Является ли обратный вызов для каждого запроса одинаковым?   -  person JoeFletch    schedule 08.11.2012
comment
По сути, он просто анализирует возвращенный HTML для определенного элемента, соответствующего запрошенному URL-адресу, и вставляет его в DOM.   -  person chrisfrancis27    schedule 08.11.2012
comment
можно ли в этом случае использовать параметр async:false?   -  person logic8    schedule 09.11.2012
comment
@logic8 К сожалению, нет, так как выполнение запросов может занять довольно много времени, а пользовательский интерфейс должен оставаться отзывчивым на протяжении всего времени.   -  person chrisfrancis27    schedule 09.11.2012


Ответы (3)


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

var urlArr = ["url1", "url2"];
var responseArr = [];
for(var i = 0; i < length; i++) {
    responseArr.push("0");//0 meaning unprocessed to the DOM
}

$.each(urlArr, function(i, url){
    $.ajax({
        url: url,
        success: function(data){
            responseArr[i] = data;
        },
        error: function (xhr, status, error) {
            responseArr[i] = "Failed Response";//enter whatever you want to place here to notify the end user
        },
        complete: function() {
           $.each(responseArr, function(i, element){
                if (responseArr[i] == "0") {
                    return;
                }
                else if (responseArr[i] != "done")
                {
                    //do something with the response
                    responseArr[i] = "done";
                }
            });
        }
    });
})
person JoeFletch    schedule 08.11.2012
comment
Итак, вы кэшируете каждый ответ внутри массива, выполняете итерацию по этому массиву ответов каждый раз, когда получаете ответ, и выходите всякий раз, когда встречаете 0 (неполный ответ)... Отлично! Это действительно интересный подход! - person chrisfrancis27; 09.11.2012
comment
Спасибо. Я обновил свой ответ, включив в него обработку ошибок для ответа в случае сбоя, и я переместил некоторый код в метод complete, чтобы он выполнялся в конце каждого вызова .ajax. - person JoeFletch; 09.11.2012
comment
ОК, в конце концов у меня все заработало! Я адаптировал ваш метод (i не передавался при закрытии, поэтому всегда было последнее значение, когда возник обратный вызов); см. мое редактирование в конце вопроса. Еще раз спасибо за то, что помогли мне думать об этом по-другому! - person chrisfrancis27; 09.11.2012

Не забывайте, что вам не нужно сразу регистрировать обратные вызовы.

Я думаю, что это сработает, главное отличие вашего кода в том, что я использовал .done, а не .then, и провел рефакторинг нескольких строк.

var promises = myUrlArray.map(function(url) {
    return $.ajax({url: url});
});

(function serialize() {
    var def = promises.shift();
    if (def) {
        def.done(function() {
            callback.apply(null, arguments);
            serialize();
        });
    }
})();
person Alnitak    schedule 08.11.2012
comment
Вау... во-первых, классный рефакторинг с использованием .map(). Итак, какая разница в использовании done() вместо then()? Насколько я понял, на самом деле не было бы разницы, если бы я не использовал возвращаемое значение обратного вызова в последующем промисе. - person chrisfrancis27; 09.11.2012
comment
Попробовав это в моем коде, он все еще выполняется в случайном порядке. Сказав это, я думаю, что мне нужно создать сокращенный тестовый пример, поскольку вполне могут быть задействованы другие силы... - person chrisfrancis27; 09.11.2012
comment
@ChrisFrancis, честно говоря, я написал это, не осознавая, насколько это близко к тому, что вы уже пробовали. На самом деле он не может выполняться в случайном порядке, потому что обратный вызов для следующего промиса даже не регистрируется, пока текущий не будет завершен. - person Alnitak; 09.11.2012
comment
Да, я думаю, что у меня где-то еще есть ошибка, которая сбивает меня с толку. Просто создаю скрипку сейчас, скоро опубликую здесь. - person chrisfrancis27; 09.11.2012

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

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

person Kevin B    schedule 08.11.2012
comment
с отложенными объектами должно быть вполне возможно заставить обратные вызовы для каждого асинхронного запроса выполняться в требуемом порядке, даже если сами запросы не будут выполняться. - person Alnitak; 09.11.2012