33

I need to make a series of N ajax requests without locking the browser, and want to use the jquery deferred object to accomplish this.

Here is a simplified example with three requests, but my program may need to queue up over 100 (note that this is not the exact use case, the actual code does need to ensure the success of step (N-1) before executing the next step):

$(document).ready(function(){

    var deferred = $.Deferred();

    var countries = ["US", "CA", "MX"];

    $.each(countries, function(index, country){

        deferred.pipe(getData(country));

    });

 });

function getData(country){

    var data = {
        "country": country  
    };


    console.log("Making request for [" + country + "]");

    return $.ajax({
        type: "POST",
        url: "ajax.jsp",
        data: data,
        dataType: "JSON",
        success: function(){
            console.log("Successful request for [" + country + "]");
        }
    });

}

Here is what gets written into the console (all requests are made in parallel and the response time is directly proportional to the size of the data for each country as expected:

Making request for [US]
Making request for [CA]
Making request for [MX]
Successful request for [MX]
Successful request for [CA]
Successful request for [US]

How can I get the deferred object to queue these up for me? I've tried changing done to pipe but get the same result.

Here is the desired result:

Making request for [US]
Successful request for [US]
Making request for [CA]
Successful request for [CA]
Making request for [MX]
Successful request for [MX]

Edit:

I appreciate the suggestion to use an array to store request parameters, but the jquery deferred object has the ability to queue requests and I really want to learn how to use this feature to its full potential.

This is effectively what I'm trying to do:

when(request[0]).pipe(request[1]).pipe(request[2])... pipe(request[N]);

However, I want to assign the requests into the pipe one step at a time in order to effectively use the each traversal:

deferred.pipe(request[0]);
deferred.pipe(request[1]);
deferred.pipe(request[2]);
George Stocker
  • 57,289
  • 29
  • 176
  • 237
Graham
  • 7,431
  • 18
  • 59
  • 84

6 Answers6

32

With a custom object

function DeferredAjax(opts) {
    this.options=opts;
    this.deferred=$.Deferred();
    this.country=opts.country;
}
DeferredAjax.prototype.invoke=function() {
    var self=this, data={country:self.country};
    console.log("Making request for [" + self.country + "]");

    return $.ajax({
        type: "GET",
        url: "wait.php",
        data: data,
        dataType: "JSON",
        success: function(){
            console.log("Successful request for [" + self.country + "]");
            self.deferred.resolve();
        }
    });
};
DeferredAjax.prototype.promise=function() {
    return this.deferred.promise();
};


var countries = ["US", "CA", "MX"], startingpoint = $.Deferred();
startingpoint.resolve();

$.each(countries, function(ix, country) {
    var da = new DeferredAjax({
        country: country
    });
    $.when(startingpoint ).then(function() {
        da.invoke();
    });
    startingpoint= da;
});

Fiddle http://jsfiddle.net/7kuX9/1/

To be a bit more clear, the last lines could be written

c1=new DeferredAjax( {country:"US"} );
c2=new DeferredAjax( {country:"CA"} );
c3=new DeferredAjax( {country:"MX"} );

$.when( c1 ).then( function() {c2.invoke();} );
$.when( c2 ).then( function() {c3.invoke();} );

With pipes

function fireRequest(country) {
        return $.ajax({
            type: "GET",
            url: "wait.php",
            data: {country:country},
            dataType: "JSON",
            success: function(){
                console.log("Successful request for [" + country + "]");
            }
        });
}

var countries=["US","CA","MX"], startingpoint=$.Deferred();
startingpoint.resolve();

$.each(countries,function(ix,country) {
    startingpoint=startingpoint.pipe( function() {
        console.log("Making request for [" + country + "]");
        return fireRequest(country);
    });
});

http://jsfiddle.net/k8aUj/1/

Edit : A fiddle outputting the log in the result window http://jsfiddle.net/k8aUj/3/

Each pipe call returns a new promise, which is in turn used for the next pipe. Note that I only provided the sccess function, a similar function should be provided for failures.

In each solution, the Ajax calls are delayed until needed by wrapping them in a function and a new promise is created for each item in the list to build the chain.

I believe the custom object provides an easier way to manipulate the chain, but the pipes could better suit your tastes.

Note : as of jQuery 1.8, deferred.pipe() is deprecated, deferred.then replaces it.

nikoshr
  • 32,926
  • 33
  • 91
  • 105
  • This answer definitely works, I'm trying to digest all of it as it is quite complex. Thank you! – Graham Dec 23 '11 at 17:25
  • I think I'm seeing this now. The main difference between my original code and yours seems to be that you are creating Deferred objects for every request, where mine was trying to use a single Deferred. Am I correct? – Graham Dec 24 '11 at 16:30
  • 1
    Some specific questions for you: (1) why do you explicitly return a promise when you are already returning the promise from the ajax call? (2) why are assiging "this" to "self"? (3) why did you not choose to use pipe() when that is the native jquery queue function? (4) When we are creating Deferred objects for each request, what is the memory requirement when I start feeding hundreds of requests into the "queue"? How lightweight is a Deferred object? – Graham Dec 24 '11 at 17:08
  • A Deferred is a promise to invoke some functions when it is marked as resolved or failed – nikoshr Dec 24 '11 at 17:44
  • Sorry for the break. Tha ajax call hasn't been made by the point I finish chaining them, so I can't use their promises and have to create my own promise, hence the custom object. Pipe could be used, but I think the intent of multiple when is clearer. Hope it helps, more explanations if you wish when I get access to a real keyboard – nikoshr Dec 24 '11 at 17:49
  • Very much appreciated, please explain more later if/when you can. I think you're on to something with when the promise is generated. – Graham Dec 25 '11 at 00:24
  • @Graham Added a solution with pipes – nikoshr Dec 27 '11 at 10:52
  • The jsfiddle for the pipes approach does not work for me. It does nothing at all when "run" is clicked. – hippietrail Jan 19 '12 at 20:03
  • @hippietrail Strange, it works for me in FF 9, Chrome 16, and even IE 9. You didn't forget to open a console, by any chance? – nikoshr Jan 20 '12 at 09:04
  • @hippietrail Added a more visual fiddle – nikoshr Jan 20 '12 at 09:14
  • You're right. I usually don't open a console on jsfiddles and expected something in the output section. It does seem to work now. – hippietrail Jan 20 '12 at 09:23
  • By the way do you think this covers the same ground as my question ***[Loop with each iteration only happening after jQuery deferred when/then possible without recursion?](http://stackoverflow.com/questions/8931563/loop-with-each-iteration-only-happening-after-jquery-deferred-when-then-possible)** - I'm still having trouble understanding it. – hippietrail Jan 20 '12 at 09:25
  • The problems are very similar, so, yes, it should solve your question. – nikoshr Jan 20 '12 at 09:51
5

Note: As of jquery 1.8 you can use .then instead of .pipe. The .then function now returns a new promise and .pipe is deprecated since it is no longer needed. See promises spec for more info about promises, and the q.js for a cleaner library of javascript promises without a jquery dependency.

countries.reduce(function(l, r){
  return l.then(function(){return getData(r)});
}, $.Deferred().resolve());

and if you like to use q.js:

//create a closure for each call
function getCountry(c){return function(){return getData(c)};}
//fire the closures one by one
//note: in Q, when(p1,f1) is the static version of p1.then(f1)
countries.map(getCountry).reduce(Q.when, Q());

Original answer:

Yet another pipe; not for the faint hearted, but a little bit more compact:

countries.reduce(function(l, r){
  return l.pipe(function(){return getData(r)});
}, $.Deferred().resolve());

Reduce documentation is probably the best place to start understanding how the above code works. Basically, it takes two arguments, a callback and an initial value.

The callback is applied iteratively over all elements of the array, where its first argument is fed the result of the previous iteration, and the second argument is the current element. The trick here is that the getData() returns a jquery deferred promise, and the pipe makes sure that before the getData is called on the current element the getData of the previous element is completed.

The second argument $.Deferred().resolve() is an idiom for a resolved deferred value. It is fed to the first iteration of the callback execution, and makes sure that the getData on the first element is immediately called.

Liam
  • 27,717
  • 28
  • 128
  • 190
topkara
  • 886
  • 9
  • 15
  • If you could comment, explain, and format this answer on more than three lines, I think it could be a very good answer. For many if not all of us this kind of thing is very difficult to grok even if we have a hunch `reduce` could be used. – hippietrail Apr 05 '13 at 07:22
  • Great FP solution, I like it – Ebrahim Byagowi Jun 27 '13 at 10:57
4

I know I'm late to this, but I believe your original code is mostly fine but has two (maybe three) problems.

Your getData(country) is being called immediately because of how you coded your pipe's parameter. The way you have it, getData() is executing immediately and the result (the ajax's promise, but the http request begins immediately) is passed as the parameter to pipe(). So instead of passing a callback function, you're passing an object - which causes the pipe's new deferred to be immediately resolved.

I think it needs to be

deferred.pipe(function () { return getData(country); });

Now it's a callback function that will be called when the pipe's parent deferred has been resolved. Coding it this way will raise the second problem. None of the getData()s will execute until the master deferred is resolved.

The potential third problem could be that since all your pipes would be attached to the master deferred, you don't really have a chain and I'm wondering if it might execute them all at the same time anyways. The docs say the callbacks are executed in order, but since your callback returns a promise and runs async, they might all still execute somewhat in parallel.

So, I think you need something like this

var countries = ["US", "CA", "MX"];
var deferred = $.Deferred();
var promise = deferred.promise();

$.each(countries, function(index, country) {
    promise = promise.pipe(function () { return getData(country); });
});

deferred.resolve();
Liam
  • 27,717
  • 28
  • 128
  • 190
Jeff Shepler
  • 2,007
  • 2
  • 22
  • 22
  • Interesting, I'll have to try this out. I didn't have the requirement to capture sequence in the return values, but it's always good to dive deeper into jquery. – Graham Sep 20 '12 at 00:25
  • I'm not sure what you're referring to with _capture sequence in the return values_. Could you elaborate? – Jeff Shepler Sep 20 '12 at 13:48
  • this is the one I like because then I can resolve it at a later time since `deferred` is stilled a Deferred object but `promise` is just a Promise object. for example, I might add something to the promise chain later on and only want to resolve it when all that work is done. But the accepted answer is still ok if you are fine with the callbacks being executed as they are added. it is still in order they are added. – gillyspy Nov 21 '13 at 03:00
4

I've had success with jQuery queues.

$(function(){
    $.each(countries, function(i,country){
      $('body').queue(function() {
        getData(country);
      });
    });
});

var getData = function(country){
  $.ajax({
    url : 'ajax.jsp',
    data : { country : country },
    type : 'post',
    success : function() {                          
      // Que up next ajax call
      $('body').dequeue();
    },
    error : function(){
      $('body').clearQueue();
    }
  });
};
Daniel Bardi
  • 352
  • 4
  • 7
4

I'm not exactly sure why you would want to do this, but keep a list of all of the URLs that you need to request, and don't request the next one until your success function is called. I.E., success will conditionally make additional calls to deferred.

ziesemer
  • 27,712
  • 8
  • 86
  • 94
  • I can't copy the exact code in here for reasons of client confidentiality, but I do have a very good reason to chain these calls sequentially. Wouldn't your solution require the whole array to be passed in to the getData function? – Graham Dec 23 '11 at 06:34
  • More or less, or otherwise available at some place above the `getData` function in the scope chain. For example, have both of your original 2 code blocks (in your original question) in a closure, combined with the array as a 3rd section. You would also need to keep track of which requests were already made - but you could handle this by just popping-off the elements as you request them (treating it like a stack). – ziesemer Dec 23 '11 at 06:38
  • 1
    It's tangential, but could you also expand on why you can't see why I'd want to queue ajax requests? There are two very good use cases I can think of: 1. limiting how many simultaneous requests are sent to a server, and 2. potential dependencies between requests. – Graham Dec 23 '11 at 06:53
  • All modern web browsers automatically manage a pool of connections per server, in an effort to improve page loading times (deferred or not). Assuming a round-trip latency of 20 ms with HTTP keep-alives enabled, 100 requests will take a minimum of 2 seconds to process sequentially - compared to maybe 5 allowed concurrent connections per hostname, this would be 20% of that, or 0.4 seconds - and this is the most conservative estimate of savings. (http://www.browserscope.org/?category=network is a good link with details around the max. concurrent connections configured in modern web browsers.) – ziesemer Dec 23 '11 at 07:03
  • Thanks for the explanation, that does eliminate the first objection. However, request dependency is my primary requirement here and I do promise that I need to queue them up (no pun intended) – Graham Dec 23 '11 at 07:14
  • I assume your original question is completely a mock example, then - as I'm not sure what dependencies you would have between countries. I do agree, though, that there are certainly use cases that could require this. – ziesemer Dec 23 '11 at 07:16
  • You are correct - it is purely a mock example. The application sends instructions into the server for each step and every case depends on the results of the previous one. The instructions contain processing logic for financial data. – Graham Dec 23 '11 at 07:34
  • Browserscope confirmed another suspicion - IE7 is restrictive in the number of concurrent requests (2) and although I haven't tested it thoroughly I do believe that the browser hangs in synchronous mode when the third request is made. I am testing in Chrome and have zero performance issues, but we do have IE7 users that are complaining about browser locking/freezing on larger sets of requests. It all makes sense now. – Graham Dec 24 '11 at 03:43
2

Update: deferred.pipe is deprecated

This is a lot of code for something that is already documented in the jQuery API. see http://api.jquery.com/deferred.pipe/

You can just keep piping them until all 100 are made.

Or, I wrote something to make N calls, and resolve a single function with the data of all the calls that have been made. Note: it returns the data not the super XHR object. https://gist.github.com/1219564

Paolo Casciello
  • 7,982
  • 1
  • 43
  • 42
Drew
  • 4,683
  • 4
  • 35
  • 50
  • At the time I originally posted this question there was very little documentation available. – Graham Apr 09 '13 at 19:45