Asynchronous Loop of jQuery Deferreds (promises)

后端 未结 6 1921
执笔经年
执笔经年 2020-12-08 08:21

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 contr

相关标签:
6条回答
  • 2020-12-08 08:43

    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);
    
    0 讨论(0)
  • 2020-12-08 08:45

    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');
    
    0 讨论(0)
  • 2020-12-08 08:48

    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

    0 讨论(0)
  • 2020-12-08 08:56

    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.

    0 讨论(0)
  • 2020-12-08 08:57

    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");
    });
    
    0 讨论(0)
  • 2020-12-08 09:00

    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.
    });
    
    0 讨论(0)
提交回复
热议问题