How can I chain together groups of promises?

2019-09-01 01:29发布

问题:

I am using the Q javascript promises library and am running in a browser, and I want to figure out how to chain together groups of promises so that each group gets executed sequentially. For example, if I have items A, B, C, and D, I want to group A and B together and then C and D together, so that both A and B must fulfill before C and D get executed. I created this simple jsfiddle to show my current attempt.

var work_items = [ 'A','B','C','D','E','F','G','H','I' ];
var n = 2;    // group size
var wait = 1000;

var getWorkPromiseFn = function (item) {
    log("Getting promise function for " + item);
    return function () {
        log("Starting " + item);
        var deferred = Q.defer();
        setTimeout(function () {
            var status = "Finished " + item;
            log(status);
            deferred.resolve(status);             
        }, wait);
        return deferred.promise;
    };
};

var queue = Q();

//log('Getting sequentially');    // One-by-one in sequence works fine
//work_items.forEach(function (item) {
//    queue = queue.then(getWorkPromiseFn(item));
//});

log('Getting ' + n + ' at a time'); // This section does not        
while (work_items.length > 0) {
    var unit = [];
    for (var i=0; i<n; i++) {
        var item = work_items.shift();
        if (item) {
            unit.push(getWorkPromiseFn(item));               
        }
    }
    queue.then(Q.all(unit));
}
var inspect = queue.inspect(); // already fulfilled, though no work is done

It looks like I am probably passing the wrong array to Q.all here, since I'm passing in an array of functions which return promises rather than an array of the promises themselves. When I tried to use promises directly there (with unit.push(Q().then(getWorkPromiseFn(item)); for example), the work for each was begun immediately and there was no sequential processing. I guess I'm basically unclear on a good way to represent the group in a way that appropriately defers execution of the group.

So, how can I defer execution of a group of promises like this?

回答1:

This can be done by first pre-processing the array of items into groups, then applying the two patterns (not the anti-patterns) provided here under the heading "The Collection Kerfuffle".

The main routine can be coded as a single chain of array methods.

var work_items = [ 'A','B','C','D','E','F','G','H','I' ];
var wait = 3000;

//Async worker function
function getWorkPromise(item) {
    console.log("Starting " + item);
    var deferred = Q.defer();
    setTimeout(function () {
        var status = "Finished " + item;
        console.log(status);
        deferred.resolve(status);             
    }, wait);
    return deferred.promise;
};

function doAsyncStuffInGroups(arr, n) {
    /* 
     * Process original array into groups, then 
     * process the groups in series, 
     * progressing to the next group 
     * only after performing something asynchronous 
     * on all group members in parallel.
     */
    return arr.map(function(currentValue, i) {
        return (i % n === 0) ? arr.slice(i, i+n) : null;
    }).filter(function(item) {
        return item;
    }).reduce(function(promise, group) {
        return promise.then(function() {
            return Q.all(group.map(function(item) {
                return getWorkPromise(item);
            }));
        });
    }, Q());
}

doAsyncStuffInGroups(work_items, 2).then(function() {
    console.log("All done");
});

See fiddle. Delay of 3s gives you time to appreciate what's going on. I found 1s too quick.

Solutions like this are elegant and concise but pretty well unreadable. In production code I would provide more comments to help whoever came after me.

For the record:

  • The opening arr.map(...).filter(...) processes arr (non destructively) into an array of arrays, each inner array representing a group of length n (plus terminal remainders).
  • The chained .reduce(...) is an async "serializer" pattern.
  • The nested Q.all(group.map(...)) is an async "parallelizer" pattern.


回答2:

The .then function of a promise does not mutate the promise, so when you do:

 p.then(function(){
         // stuff
 });

You do not change the promise p at all, instead, you need to assign it to something:

p = p.then(....)

This is why your queue promise was always resolved, it never changed beyond Q().

In your case, something like changing:

queue.then(Q.all(unit));

Into:

queue = queue.then(function(){ return Q.all(unit); });

Or in ES6 promises and libraries that use their syntax like Bluebird the other answer mentioned:

queue = queue.then(function(){ return Promise.all(unit); });


回答3:

The thing that confused me most is that the async function being chained needs to return a function that returns a promise. Here's an example:

function setTimeoutPromise(ms) {
  return new Promise(function (resolve) {
    setTimeout(resolve, ms);
  });
}

function foo(item, ms) {
  return function() {
    return setTimeoutPromise(ms).then(function () {
      console.log(item);
    });
  };
}

var items = ['one', 'two', 'three'];

function bar() {
  var chain = Promise.resolve();
  for (var i in items) {
    chain = chain.then(foo(items[i], (items.length - i)*1000));
  }
  return chain.then();
}

bar().then(function () {
  console.log('done');
});

Notice that foo returns a function that returns a promise. foo() does not return a promise directly.

See this Live Demo



回答4:

i would suggest you use bluebird, its the best performance promise out there, https://github.com/petkaantonov/bluebird

the example to chain should also be here https://github.com/petkaantonov/bluebird#how-do-long-stack-traces-differ-from-eg-q