2

I'm having a hard time wrapping my mind around jQuery promises. I've created the following snippet to explore jQuery promises; informed by this StackOverflow entry.

let q = [
  function () { setTimeout(function () { console.log("a - " + Date.now()); }, 5000); },
  function () { setTimeout(function () { console.log("b - " + Date.now()); }, 2500); },
  function () { setTimeout(function () { console.log("c - " + Date.now()); }, 0); }
];
    
function SerialCall(queue) {
  var d = $.Deferred().resolve();
   while (queue.length > 0) {
     d = d.then(queue.shift()); // you don't need the `.done`
   }
}

SerialCall(q);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

My understanding is that jQuery promises should hold off executing b and c while a is executing and subsequently hold off executing c while b is executing.

I was expecting the output to be a, b, c but I'm getting c, b, a. Note that I deliberately picked these delays ('a': 5000, 'b': 2500, 'c':0) to illustrate the fact that jQuery promises aren't blocking execution as planned.

What am I missing and how should I alter the code to get the expected behavior?

Frank
  • 590
  • 1
  • 8
  • 23

3 Answers3

2

WARNING: this answer only addresses the I was expecting the output to be a, b, c but I'm getting c, b, a.. It not solves the promise issue.

In your code, whatever you hope for (as explained by Michael Geary above), 'a' outputs after 5 seconds, 'b' ouputs after 2.5 seconds and 'c' outputs immediatly.

If you want the 'a' to ouptut before the 'c', its waiting time (its timeout) must be shorter.

let queue = [
    function () { 
      let waitingTime = 0 ;
      setTimeout(function () { console.log("a - " + Date.now()); }, waitingTime); },
    function () { 
      let waitingTime = 2500 ;
      setTimeout(function () { console.log("b - " + Date.now()); }, waitingTime); },
    function () { 
      let waitingTime = 5000 ;
      setTimeout(function () { console.log("c - " + Date.now()); }, waitingTime); }
];

function SerialCall(queue) {
    var d = $.Deferred().resolve();
    while (queue.length > 0) {
        d = d.then(queue.shift()); // you don't need the `.done`
    }
}

SerialCall(queue);
.as-console-wrapper{max-height:100%!important;top:0;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

By the way, the clearer is your code, the simplier you can debug it, understand it. Look for instance:

let queue = [
    function () { waitForMe('a', 0)    },
    function () { waitForMe('b', 2500) },
    function () { waitForMe('c', 5000) }
];

function SerialCall(queue) {
    var d = $.Deferred().resolve();
    while (queue.length > 0) {
        d = d.then(queue.shift()); // you don't need the `.done`
    }
}

function waitForMe(letter, someTime) {
  return setTimeout(function () { console.log(letter +" - " + Date.now()); }, someTime)
}

SerialCall(queue);
.as-console-wrapper{max-height:100%!important;top:0;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
  • Thanks for your response, I deliberately picked these delays to illustrate the fact that jQuery promises aren't blocking execution as planned. – Frank Oct 29 '17 at 05:11
  • Philcare, I'm trying to wrap my mind around jQuery promises. In my example the simple setTimeout() calls serve as proxies for more complex functions I wish to run sequentially. Your answer unfortunately doesn't help address this challenge. – Frank Oct 29 '17 at 05:27
  • Per my issue: "My understanding is that jQuery promises should hold off executing `b` and `c` while `a` is executing and subsequently hold off executing `c` while `b` is executing." How should I alter the code to get the expected behavior? – Frank Oct 29 '17 at 05:41
1

Where you are going wrong is in thinking that a promise blocks execution. It doesn't. A promise is just another way to write a callback function; it doesn't add any magic beyond what JavaScript natively offers. (I'm not addressing async/await here, just "traditional" JavaScript.) All of your code runs to completion before any callbacks get called.

That's where you're running into trouble. Your while loop runs to completion before any of your queue functions ever get called:

while (queue.length > 0) {
    d = d.then(queue.shift()); // you don't need the `.done`
}

If you want to have multiple functions get called with timeouts like you're doing, the best way to do it is not to fire all the setTimeout() calls all in a single gulp as your code does now. Instead, have each of the callback functions start the next setTimeout(). This way you only have a single timeout pending at any time, and when it fires you start the next one.

I wrote a jQuery plugin many years ago for this called slowEach. (It doesn't really depend on jQuery and the same technique would work for non-jQuery code too.)

The code doesn't use promises - it predates them by several years - but the principle is the same whether you use promises or traditional callbacks: start with a single setTimeout(), and then when its callback function gets called, start the next setTimeout(). This sequences the timeouts in the manner you were probably hoping the simple while loop would do.

The original slowEach() code can be found in this answer. Several people have made improvements to the code since then. In particular, here is a version that adds an onCompletion callback so you get a different callback when the entire array has been processed.

This code doesn't use promises, but more importantly, it works. :-) It would be an interesting exercise to adapt the code to use promises.

But again, don't assume that JavaScript will wait for a promise to be done before it executes the next line of code. Unless you're using async/await, whatever ordinary loop you're running - while, for, whatever - will always run to completion before any of the callbacks - ordinary callbacks or promises - get run.

Michael Geary
  • 28,450
  • 9
  • 65
  • 75
0

I suggest to use this jQuery extension to make sequential execution in a deferred queue.

You can use your list of timeout function as input of deferQueue, and execute them inside a main function (callable).

$.fn.deferQueue = function(callable, options){
    options = Object(options);
  var it = (this.get() || [])[Symbol.iterator]();
  var stop = false, cond = null;
  var self = this;
  this.stop = function(){ stop=true; };
  this.end = function(_cond){ cond = _cond };
  var tid = 0;
  var iterate = function(){
   if(tid) clearTimeout(tid);
   var o = it.next();
   if(cond instanceof Function && cond(o.value)) return;
   if(o.done || stop) return;
   var d = callable.call(self, o.value);
   if(options.timeout) tid = setTimeout(function(){ d.reject('timeout'); }, options.timeout);
   if(options.success) d.done(options.success);
   if(options.fail) d.fail(options.fail);
   d[options.iterate || 'always'](iterate);
  }
  iterate();
  return this;
 }
  
  function log(text){
   console.log('Log: '+text);
  }
  function error(text){
   console.log('Error: '+text);
  }
  
  let q = [
  function (D) { setTimeout(function () { console.log("a - " + Date.now()); D.resolve('function 1'); }, 5000); },
  function (D) { setTimeout(function () { console.log("b - " + Date.now()); D.resolve('function 2') }, 2500); },
  function (D) { setTimeout(function () { console.log("c - " + Date.now()); D.resolve('function 3') }, 0); },
'https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png',
'https://unreachabe_domain/image.png',
'https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_150x54dp.png',
null,
'https://www.google.com/images/branding/googlelogo/1x/googlelogo_color_272x92dp.png'
];
  
  $(function(){       
    var
      display_timeout = parseInt($('#container').data('display-timeout')),
      loading_timeout = parseInt($('#container').data('loading-timeout'));
      
    $(q).deferQueue(function(e){
     var D = $.Deferred();
      if(typeof(e) == 'string') $('<img/>').attr('src',e).on('load',function(){ setTimeout(function(){ D.resolve(e); },display_timeout)}) 
        .on('error', function(){ D.reject(e) })
       .appendTo('#container');
      else if(e instanceof Function) e(D);
      D.done(log).fail(error);
      return D;
    },{iterate:'always',timeout:loading_timeout}).end(function(e){ return e===null; });
  
  })
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<pre>
  usage : $(&lt;iterable&gt;).deferQueue(&lt;function(element)&gt;,&lt;options&gt;)
  
  options:
      timeout: int(seconds)
      iterate: string(always | done | fail)
      success: function
      fail: function
</pre>
<div id="container" data-display-timeout="1000" data-loading-timeout="3000">

</div>
Chouettou
  • 1,009
  • 1
  • 9
  • 10
  • Code only answers arent encouraged as they dont provide much information for future readers please provide some explanation to what you have written – WhatsThePoint Feb 07 '18 at 09:37