Question

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.

Was it helpful?

Solution

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');

OTHER TIPS

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

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

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

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.

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);
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top