Asynchronous Loop of jQuery Deferreds (promises)
Asked Answered
E

6

22

I am trying to create what I think is referred to as a "Waterfall". I want to sequentially process an array of async functions (jQuery promises).

Here's a contrived example:

function doTask(taskNum){
    var dfd = $.Deferred(), 
        time = Math.floor(Math.random()*3000);

    setTimeout(function(){
        console.log(taskNum);
        dfd.resolve();
    },time)

    return dfd.promise();
}

var tasks = [1,2,3];

for (var i = 0; i < tasks.length; i++){
    doTask(tasks[i]);
}

console.log("all done");

I would like it to complete the task in the order they are executed (present in the array). So, in this example I want it to do task 1 and wait for it to resolve then do task 2 wait for it to resolve, do task 3 etc and the log "all done".

Maybe this is really obvious but I've been trying to figure this out all afternoon.

Evacuate answered 19/3, 2013 at 16:21 Comment(0)
J
22

I'd try using $().queue instead of $.Deferred here. Add the functions to a queue, and only call the next one when ready.

function doTask(taskNum, next){
    var time = Math.floor(Math.random()*3000);

    setTimeout(function(){
        console.log(taskNum);
        next();
    },time)
}

function createTask(taskNum){
    return function(next){
        doTask(taskNum, next);
    }
}

var tasks = [1,2,3];

for (var i = 0; i < tasks.length; i++){
    $(document).queue('tasks', createTask(tasks[i]));
}

$(document).queue('tasks', function(){
    console.log("all done");
});

$(document).dequeue('tasks');
Jovitta answered 19/3, 2013 at 18:8 Comment(1)
this is one of a kind! solved a very hard problem for me. thank you very much.Dav
G
9

For a waterfall, you need an async loop:

(function step(i, callback) {
    if (i < tasks.length)
        doTask(tasks[i]).then(function(res) {
            // since sequential, you'd usually use "res" here somehow
            step(i+1, callback);
        });
    else
        callback();
})(0, function(){
    console.log("all done");
});
Gadfly answered 19/3, 2013 at 17:26 Comment(0)
C
8

You can create a resolved $.Deferred and just add to the chain with each iteration:

var dfd = $.Deferred().resolve();
tasks.forEach(function(task){
    dfd = dfd.then(function(){
        return doTask(task);
    });
});

Step by step the following is happening:

//begin the chain by resolving a new $.Deferred
var dfd = $.Deferred().resolve();

// use a forEach to create a closure freezing task
tasks.forEach(function(task){

    // add to the $.Deferred chain with $.then() and re-assign
    dfd = dfd.then(function(){

        // perform async operation and return its promise
        return doTask(task);
    });

});

Personally, I find this cleaner than recursion and more familiar than $().queue (jQuery API for $().queue is confusing as it is designed for animations, it is also likely you are using $.Deferred's in other places in your code). It also has the benefits of standard transfer of results down the waterfall through resolve() in the async operation and allowing the attachment of a $.done property.

Here it is in a jsFiddle

Clanton answered 21/11, 2015 at 3:19 Comment(0)
I
5

Have a look at the $.when and then methods for running deferreds.

Waterfalls are used to pipe return values from one deferred to the next, in series. It would look something like this.

function doTask (taskNum) {
  var dfd = $.Deferred(),
      time = Math.floor(Math.random() * 3000);

  console.log("running task " + taskNum);

  setTimeout(function(){
      console.log(taskNum + " completed");
      dfd.resolve(taskNum + 1);
  }, time)

  return dfd.promise();
}

var tasks = [1, 2, 3];

tasks
  .slice(1)
  .reduce(function(chain) { return chain.then(doTask); }, doTask(tasks[0]))
  .then(function() { console.log("all done"); });

Note the argument passed to resolve. That gets passed to the next function in the chain. If you just want to run them in series without piping in arguments, you can take that out and change the reduce call to .reduce(function(chain, taskNum) { return chain.then(doTask.bind(null, taskNum)); }, doTask(tasks[0]));

And in parallel it would look like this:

var tasks = [1,2,3].map(function(task) { return doTask(task); });

$.when.apply(null, tasks).then(function() { 
    console.log(arguments); // Will equal the values passed to resolve, in order of execution.
});
Intraatomic answered 19/3, 2013 at 17:52 Comment(1)
using reduce and map in this way is super elegantClanton
U
0

Interesting challenge indeed. What I have come up with is a recursive function that accepts a list and an optional start index.

Here is a link to the jsFiddle that I have tested with a few different list lengths and intervals.

I'm assuming you have a list of functions that return promises (not a list of numbers). If you do have a list of numbers you would change this part

$.when(tasks[index]()).then(function(){
    deferredSequentialDo(tasks, index + 1);
});

to this

/* Proxy is a method that accepts the value from the list
   and returns a function that utilizes said value
   and returns a promise  */
var deferredFunction = myFunctionProxy(tasks[index]);

$.when(tasks[index]()).then(function(){
    deferredSequentialDo(tasks, index + 1);
});

I'm not sure how big your list of functions could be but just be aware that the browser will hold on to the resources from the first deferredSequentialDo call until they are all finished.

Urinary answered 19/3, 2013 at 17:23 Comment(6)
deferredSync sounds like an oxymoronGadfly
It does, however I could see a use for this if you have a couple of ajax calls that you want to execute synchronously (something I have in fact come across in my experience)Urinary
Ajax is not synchronous. What do you mean?Gadfly
I know it's not synchronous, but if ajax call #2 depends on ajax call #1's result, then they need to be executed 1 after the other, not at the same time.Urinary
But that's "sequential", and "deferred" is the correct term. Nothing syncGadfly
Yeah my bad, my brain clobbered sequential and synchronous together. Updated my answer and jsFiddle.Urinary
P
0

Arguments

  • items: array of arguments
  • func: the async function
  • callback: callback function
  • update: update function

Simple Loop:

var syncLoop = function(items, func, callback) {
    items.reduce(function(promise, item) {
        return promise.then(func.bind(this, item));
    }, $.Deferred().resolve()).then(callback);
};

syncLoop(items, func, callback);

Track Progress:

var syncProgress = function(items, func, callback, update) {
    var progress = 0;
    items.reduce(function(promise, item) {
        return promise.done(function() {
            update(++progress / items.length);
            return func(item);
        });
    }, $.Deferred().resolve()).then(callback);
};

syncProgress(items, func, callback, update);
Pneumoconiosis answered 9/1, 2019 at 21:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.