Using $.Deferred() with nested ajax calls in a loop

12,018

Solution 1

Yeah, using Deferred is the way to do that:

function a() {
    var def = $.Deferred();

    $.ajax("http://url1").done(function(data){
        var requests = [];

        for (var i = 0; i < 2; i++) {
             requests.push(b());
        }

        $.when.apply($, requests).then(function() { def.resolve(); });
    });

    return def.promise();
}

// called by a()
function b() {
    var def = $.Deferred(),
        requests = [];

    for (var i = 0; i < 2; i++) {
        requests.push($.ajax("http://url2").done(function(data){
            // do something
            console.log('b called');
        });
    }

    $.when.apply($, requests).then(function() { def.resolve(); });

    return def.promise();
}

function main(){
    $.when(a()).done(function(){
        console.log('all completed');
    });
}

//EDIT: Replaced .pipe with .done.

Solution 2

You could use an Array which is located in a higher context to push Promise / Deferred objects into. Then you could then use jQuery.when alongside Function.prototype.apply to pass all entries as arguments.

(function() {
    var promises = [ ],
        when = Function.prototype.apply.bind( jQuery.when, null );

    function a() {
         promises.push($.ajax("http://url1").pipe(function(data){
             for (var i = 0; i < 2; i++) {
                 console.log('a called');
                 b();
             }
         }));

         return promises;
    }

    function b() {
        for (var i = 0; i < 2; i++) {
            promises.push($.ajax("http://url2", function(data) {
                // do something
                console.log('b called');
            }));
        }
    }

    function main() {
        promises = [ ];

        when( a() ).done(function(){
            console.log('all completed');
        });
    }
}());

Solution 3

The question might be old, but since there's no correct solution yet I'll put an answer here. It properly chains the promises by using .then (previsouly been .pipe) to achieve the requested result:

function a() {
  return $.ajax("http://url1").done(function(data){
    console.log('a called');
  }).then(function(){
    return $.when(b(), b()); // no loop for simplicity
  });
}
function b() {
  return $.ajax("http://url2").done(function(data){
    console.log('b called');
  });
}

function main(){
  a().done(function(){
    console.log('all completed');
  }, function() {
    console.log('an error occured!');
  });
}

Depending on which result data should be available where the nesting/structure might be changed, but the overall ordering is correct.

Solution 4

I believe this can be fixed with callbacks, but a fiddle would have really helped me check for you.

 // called by main()
 function a(callback) {
   //set this to the number of loops that is going to happen
   var number = 2;
   return $.ajax("http://url1", function(data){
     console.log('a called');
     for (var i = 0; i < number ; i++) {
       b();
       if(number===i){
           callback();
       }
     }
   }
 }

 function main(){
    a(function(){
       //Function or code you want to run on completion.. 
    });
 }

Forgive me if this doesn't work, but i think its the right direction.

Share:
12,018
jokkedk
Author by

jokkedk

Founder of Danish accounting startup Simpelt Regnskab and Software Architect at Scienta. Author of developer tools Webgrind and ZFDebug. Working on the next great thing.

Updated on June 15, 2022

Comments

  • jokkedk
    jokkedk about 2 years

    I've spent far too many hours searching for similar questions and trying solutions, so I hope someone has a solution.

    Basically, I would like to be notified when a function a() has completed. The problem is that the function contains an ajax call and a loop that calls b(), which again contains an ajax call.

    UPDATED WITH FIDDLE: http://jsfiddle.net/hsyj7/1/

    Like so:

    // called by main()
    function a() {
      return $.ajax("http://url1").pipe(function(data){
        for (var i = 0; i < 2; i++) {
          console.log('a called');
          b();
        }
      });
    }
    
    // called by a()
    function b() {
      for (var i = 0; i < 2; i++) {
        $.ajax("http://url2", function(data){
          // do something
          console.log('b called');
        }
      }
    }
    
    function main(){
      $.when(a()).done(function(){
        console.log('all completed');
      });
    }
    

    What I would like to see then is, possibly with both calls to a() at the top:

    a called
    b called
    b called
    a called
    b called
    b called
    all completed
    

    Instead I get

    a called
    all completed
    b called
    b called
    

    Or some variant thereof.

    I am aware that the above code is missing defer functionality in both the loop and in b(). In some of the variants I have tried, the done() handler in main() is never called.

    Any one know how to do this?

  • Stuart Burrows
    Stuart Burrows over 11 years
    Here's a fiddle of this solution: jsfiddle.net/pxVAE/1. Doesn't quite work, might be in my transcribing...
  • Matt
    Matt over 11 years
    I'd +1, but I'm out of votes. Just FMI, what's being achieved by using pipe() in a() (or in this case is it just synonymous with using then()?)
  • freakish
    freakish over 11 years
    @Matt In this piece of code? Absolutely nothing. I assume it is a snippet from larger piece of code.
  • freakish
    freakish over 11 years
    -1: Multiple calls to a will cause a memory leak, because promises is a "global" list.
  • jAndy
    jAndy over 11 years
    @freakish: its not a global list. OP didn't say anything about multiple calls. Even if that was the case, it would only push more objects into the Array which definitely is not a memory leak. He would just need to re-initialize the Array. -1 for that? Are you joking ?
  • freakish
    freakish over 11 years
    That's why I wrote global in quation marks. Global from a and b point of view. OP clearly gave example of a being called twice. The memory leak is there, because you only push to promises.
  • jAndy
    jAndy over 11 years
    @freakish: you better google for the definition of a memory leak bro, pushing data into an array is just not that. But again, resetting that to an empty Array is open for everyone. Again, this is not a downvote. Are you joking..
  • freakish
    freakish over 11 years
    Then why didn't you include resetting in your solution? The code leaks memory when called multiple times. True or false? I don't understand why are you constantly trying to make fun of me. Write your solution properly, then I will upvote.
  • Felix Kling
    Felix Kling over 11 years
    @freakish: Actually, a() is only called once in the OP's example. And since all the functions in jAndy's solution are inside a function expression, none of them can actually called ;) That's more worth to be pointed out :)
  • freakish
    freakish over 11 years
    @FelixKling Oh, you're right. Sorry, I thought that console.log is before loop. Fair enough, I'll upvote now.
  • jAndy
    jAndy over 11 years
    @freakish: I don't want a flamewar. But you should seriously re-think your behavior. Your wording in this ECMAcript context is pretty questionable (quotation or not and about memory leaks) + the fact that its not stated the function is getting called more than once.
  • freakish
    freakish over 11 years
    @jAndy Re-think my behavior? I'm very patient and polite. I'm not your "bro" and I like to use quation marks ( which is not difficult to understand, seriously ) and I don't make fun of other people. Maybe you should re-think your behavior? Now I've made a mistake and I apologize.
  • freakish
    freakish over 11 years
    @FelixKling True, however we all know, that this is just a snippet and perhaps this is bound to some event. Anyway potentially it can lead to memory leak.
  • Raffaele
    Raffaele over 11 years
    @jAndy I think you could even scope and return promises inside both of a() and b(), to get rid of context state altogether
  • Felix Kling
    Felix Kling over 11 years
    @freakish: Exactly, it's just a snippet... no need to rumble too much about it... ease up everyone, it's Christmas time :)
  • jAndy
    jAndy over 11 years
    @freakish: I didn't make any fun of you, I was just shocked that I seriously got a downvote, with that reason. The reason wasn't even correct or true (by definition), so that is what I call a wrong behavior on this site.
  • jokkedk
    jokkedk over 11 years
    Thanks @freakish, that works! My confusion was when to use resolve() and promise() in the loops.
  • jAndy
    jAndy over 11 years
    @FelixKling: what do you mean with none of them can actually called ? Code works as expected.
  • Felix Kling
    Felix Kling over 11 years
    Where are you calling main()?
  • jAndy
    jAndy over 11 years
    @FelixKling: where does the OP call it ?
  • Felix Kling
    Felix Kling over 11 years
    Fair enough, I just wanted to point out that none of the functions a, b and main are accessible from outside the whole code block. So assuming OP calls main somewhere, it could not be found with your code.
  • freakish
    freakish over 11 years
    @jAndy Hehe, so maybe we should be very strict and by definition ( whatever that means ) answer that the code works correct, because it is not called anywhere. Question closed. Because OP didn't state it. You are so stubborn, while everyone knows that the code you've shown us potentially leads to memory leak. Also it can be easily fixed, so I don't understand this entire attack. And stop offending me all the time. Not correct? Not true? B***ch, please.
  • jAndy
    jAndy over 11 years
    @freakish: I truly believe that I'm pretty open minded and not stubborn. But you just "assume" things and based on that, predict memory leaks (which still are no memory leaks, still, by definition) and dangerously fool around with words like global which are generally used in a whole different way. That plus of course the downvote for this questionable overall behavior. What did you expect me to do ? Say thank you ?
  • freakish
    freakish over 11 years
    @jAndy Is it that difficult to assume it? Is it so unlikely to happen? You did assume that main is called somewhere, right ( otherwise the code works by definition )? So I assumed that it is possibly called multiple times. How is it different? And do you have to be so stubborn about "global" word, you grammar freak? Not to mention that we should also teach good practices and what you've shown us is not even close. You don't have to say anything, just fix your solution. OK, that's it for me. I won't argue anymore, as Felix stated is is Christmas time after all. Merry Christmas!
  • jAndy
    jAndy over 11 years
    @freakish: its not difficult, but quite leaning far out of the window. It's one thing to expect that a code gets executed once (otherwise there is no error or questions) but a different story to predict or assume unknown application logic. And you call me stubborn ;) Beeing picky on your global expression is still correct imho. It wouldn't on any other tag, but this question is tagged with javascript. Merry xmas.
  • freakish
    freakish over 11 years
    @jAndy OMG, fair enough, I apologize for misusing "global" word. :) I won't apologize for calling you stubborn, though. :) Merry Christmas!
  • Bergi
    Bergi over 10 years
    -1 for not handling errors. Also, why do you use the pipe method when you don't utilize it?
  • Bergi
    Bergi over 10 years
    -1. The promises.push in b() is too late, $.when() will not account for them.
  • freakish
    freakish over 10 years
    @Bergi Am I supposed to write all of the code for OP? I've simply shown him how to achieve what he wants.
  • Bergi
    Bergi over 10 years
    No, but it would help if the bad practise of constructing $.Deferreds does not spread :-)
  • freakish
    freakish over 10 years
    @Bergi I agree ( I'll update the question once I have enough time to recall what was it even about :D ). However since it does answer the question I don't think it deserves -1.
  • Bergi
    Bergi over 10 years
    I think (high-voted) bad-practise answers are a hazard (to the >1k viewers), so I pressed "answer is not useful". I'm looking forward to see it corrected!
  • freakish
    freakish over 10 years
    @Bergi I've fixed it. It should be ok now.
  • Bergi
    Bergi over 10 years
    Usage of pipe was not the problem - it would've been the solution actually. The problem is manually constructing and resolving deferreds. You might want to have a look a my answer below.
  • freakish
    freakish over 10 years
    @Bergi Then we completely disagree. You've moved a big part of function a() outside of it. What if it is called somewhere else directly? I can't agree to such refactoring. We should not modify the original code more then necessary.
  • Bergi
    Bergi over 10 years
    Sure - check my update. This triviality of the change is the reason to use then for composing asynchronous actions.
  • freakish
    freakish over 10 years
    @Bergi I see, it seems to be fine now (there are still many things for me to learn about jQuery, indeed). However I disagree that using deferreds directly is a problem. Longer? Yes. Incorrect? No. It's just another solution to the problem. I'll leave the answer as it is. People can use your solution, I don't mind. I'll even upvote it.
  • Bergi
    Bergi over 10 years
    It's an error-prone design. People forget to reject the deferreds when they would need - just like you have. Using then automatically takes care of that.
  • freakish
    freakish over 10 years
    @Bergi I didn't forget about it - I just didn't care. I agree that it is error-prone but that doesn't mean that it is incorrect. Your solution might be better, but it doesn't mean that mine is wrong.
  • Darknight
    Darknight almost 8 years
    @freakish , with reference to your answer I tried a sample to execute a deferred function in loop. However the result is not working as expected. Refer - jsfiddle.net/fewtalks/nvbp26sx/1. This prints output as {{ processed test1 item 0; processed test1 item 1; processed test item 0; processed test item 1; completed}} . However I expect the output as {{ processed test1 item0 processed test item0; processed test1 item1; processed test item0}}. Why the deferred test function is not executing after test1 is completed.
  • freakish
    freakish almost 8 years
    @fewtalks Post this as a new question (with full code copied there) and we'll have a look at it. It's hard to read from your comment what is the expected output.
  • Darknight
    Darknight almost 8 years