Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

jQuery Deferred/Promises dynamic array not executing callbacks in correct order

Grateful for any insight into what I'm misunderstanding here. My requirement is as follows:

I have an array of URLs. I want to fire off an AJAX request for each URL simultaneously, and as soon as the first request completes, call the first callback. Then, if and when the second request completes, call that callback, and so on.

Option 1:

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

Obviously this doesn't work, as there is no guarantee the responses will complete in the correct order.

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

This should work, but the downside is that it waits until all AJAX requests have completed, before firing any of the callbacks.

Ideally, I should be able to call the first callback as soon as it's complete, then chain the second callback to execute whenever that response is received (or immediately if it's already resolved), then the third, and so on.

The array length is completely variable and could contain any number of requests at any given time, so just hard coding the callback chain isn't an option.

My attempt:

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

The problem is that the callbacks execute in a completely random order! For example, if I add 'home.html', 'page2.html', 'page3.html' to the array, the order of responses won't necessarily be 'home.html', 'page2.html', 'page3.html'.

I'm obviously fundamentally misunderstanding something about the way promises work. Any help gratefully appreciated!

Cheers

EDIT

OK, now I'm even more confused. I made this JSFiddle with one array using Alnitak's answer and another using JoeFletch's answer and neither of them work as I would expect! Can anyone see what is going on here?

EDIT 2

Got it working! Based on JoeFletch's answer below, I adapted the solution as follows:

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: I don't understand jQuery promises, got it working without them. :)

like image 371
chrisfrancis27 Avatar asked Nov 08 '12 19:11

chrisfrancis27


3 Answers

Don't forget that you don't need to register the callbacks straight away.

I think this would work, the main difference with your code being that I've used .done rather than .then and refactored a few lines.

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();
        });
    }
})();
like image 177
Alnitak Avatar answered Nov 14 '22 21:11

Alnitak


Here's my attempt at solving this. I updated my answer to include error handling for a failed .ajax call. I also moved some code to the complete method of the .ajax call.

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";
                }
            });
        }
    });
})
like image 34
JoeFletch Avatar answered Nov 14 '22 23:11

JoeFletch


Asynchronous requests aren't guaranteed to finish in the same order that they are sent. some may take longer than others depending on server load and the amount of data being transferred.

The only options are either to wait until they are all done, only send one at a time, or just deal with them being called possibly out of order.

like image 21
Kevin B Avatar answered Nov 14 '22 23:11

Kevin B