最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Implementing Promise.series as alternative to Promise.all - Stack Overflow

programmeradmin0浏览0评论

I saw this example implementation of Promise.all - which runs all promises in parallel - Implementing Promise.all

Note that the functionality I am looking for is akin to Bluebird's Promise.mapSeries .html

I am making an attempt at creating Promise.series, I have this which seems to work as intended (it actually is totally wrong, don't use it, see answers):

Promise.series = function series(promises){

    return new Promise(function(resolve,reject){

    const ret = Promise.resolve(null);
    const results = [];

    promises.forEach(function(p,i){
         ret.then(function(){
            return p.then(function(val){
               results[i] = val;
            });
         });
    });

    ret.then(function(){
         resolve(results);
    },
     function(e){
        reject(e);
     });

    });

}


Promise.series([
    new Promise(function(resolve){
            resolve('a');
    }),
    new Promise(function(resolve){
            resolve('b');
    })
    ]).then(function(val){
        console.log(val);
    }).catch(function(e){
        console.error(e.stack);
    });

However, one potential problem with this implementation is that if I reject a promise, it doesn't seem to catch it:

 Promise.series([
    new Promise(function(resolve, reject){
            reject('a');   // << we reject here
    }),
    new Promise(function(resolve){
            resolve('b');
    })
    ]).then(function(val){
        console.log(val);
    }).catch(function(e){
        console.error(e.stack);
    });

does anyone know why the error doesn't get caught and if there is a way to fix this with Promises?

According to a comment, I made this change:

Promise.series = function series(promises){

    return new Promise(function(resolve,reject){

    const ret = Promise.resolve(null);
    const results = [];

    promises.forEach(function(p,i){
         ret.then(function(){
            return p.then(function(val){
               results[i] = val;
            },
            function(r){
                console.log('rejected');
                reject(r);   // << we handle rejected promises here
            });
         });
    });

    ret.then(function(){
         resolve(results);
    },
     function(e){
        reject(e);
     });

    });

}

but this still doesn't work as expected...

I saw this example implementation of Promise.all - which runs all promises in parallel - Implementing Promise.all

Note that the functionality I am looking for is akin to Bluebird's Promise.mapSeries http://bluebirdjs.com/docs/api/mapseries.html

I am making an attempt at creating Promise.series, I have this which seems to work as intended (it actually is totally wrong, don't use it, see answers):

Promise.series = function series(promises){

    return new Promise(function(resolve,reject){

    const ret = Promise.resolve(null);
    const results = [];

    promises.forEach(function(p,i){
         ret.then(function(){
            return p.then(function(val){
               results[i] = val;
            });
         });
    });

    ret.then(function(){
         resolve(results);
    },
     function(e){
        reject(e);
     });

    });

}


Promise.series([
    new Promise(function(resolve){
            resolve('a');
    }),
    new Promise(function(resolve){
            resolve('b');
    })
    ]).then(function(val){
        console.log(val);
    }).catch(function(e){
        console.error(e.stack);
    });

However, one potential problem with this implementation is that if I reject a promise, it doesn't seem to catch it:

 Promise.series([
    new Promise(function(resolve, reject){
            reject('a');   // << we reject here
    }),
    new Promise(function(resolve){
            resolve('b');
    })
    ]).then(function(val){
        console.log(val);
    }).catch(function(e){
        console.error(e.stack);
    });

does anyone know why the error doesn't get caught and if there is a way to fix this with Promises?

According to a comment, I made this change:

Promise.series = function series(promises){

    return new Promise(function(resolve,reject){

    const ret = Promise.resolve(null);
    const results = [];

    promises.forEach(function(p,i){
         ret.then(function(){
            return p.then(function(val){
               results[i] = val;
            },
            function(r){
                console.log('rejected');
                reject(r);   // << we handle rejected promises here
            });
         });
    });

    ret.then(function(){
         resolve(results);
    },
     function(e){
        reject(e);
     });

    });

}

but this still doesn't work as expected...

Share Improve this question edited May 23, 2017 at 10:29 CommunityBot 11 silver badge asked Jun 1, 2016 at 21:07 Alexander MillsAlexander Mills 100k165 gold badges531 silver badges908 bronze badges 12
  • 2 You're not catching the inner promise. Try to use reduce() instead of forEach, chaining them by then – salezica Commented Jun 1, 2016 at 21:21
  • 5 But it doesn't "run" promises. Promise.series gets already created promises that are running right after they are created. – Eugene Commented Jun 1, 2016 at 21:30
  • 3 No, when you create promise via new Promise(executor) the executor function is invoked upon creation (moreover synchronously). Inside Promise.series you can only wait for promises fulfillment/rejection. – Eugene Commented Jun 1, 2016 at 21:38
  • 4 @AlexMills No, then() doesn't "start" a promise. Both Promise.all([$.get("foo.html"), $.get("bar.html")]) and Promise.series([$.get("foo.html"), $.get("bar.html")]) would immediately start 2 AJAX requests in parallel and then await their results. If you want the next request to start after the previous one completed, you need to give Promise.series an array of "promise factories": functions that create (and thus "start") a promise. – Mattias Buelens Commented Jun 1, 2016 at 21:40
  • 4 I would pass array of functions (and call it tasks) to the Promise.series and execute them in series. Passing array of promises just does not make sense for me - it works almost the same as Promise.all – Eugene Commented Jun 1, 2016 at 21:41
 |  Show 7 more comments

4 Answers 4

Reset to default 4

The promise returned by then in the forEach loop does not handle potential errors.

As pointed out in a comment by @slezica, try to use reduce rather than forEach, this chains all promises together.

Promise.series = function series(promises) {
    const ret = Promise.resolve(null);
    const results = [];

    return promises.reduce(function(result, promise, index) {
         return result.then(function() {
            return promise.then(function(val) {
               results[index] = val;
            });
         });
    }, ret).then(function() {
        return results;
    });
}

Keep in mind that the promises are already "running" at that point though. If you truly want to run your promises in series, you should adjust your function and pass in an array of functions that return promises. Something like this:

Promise.series = function series(providers) {
    const ret = Promise.resolve(null);
    const results = [];

    return providers.reduce(function(result, provider, index) {
         return result.then(function() {
            return provider().then(function(val) {
               results[index] = val;
            });
         });
    }, ret).then(function() {
        return results;
    });
}

This is a common misunderstanding of how promises work. People want there to be a sequential equivalent to the parallel Promise.all.

But promises don't "run" code, they're mere return values one attaches completion callbacks to.

An array of promises, which is what Promise.all takes, is an array of return values. There's no way to "run" them in sequence, because there's no way to "run" return values.

Promise.all just gives you one promise representing many.

To run things in sequence, start with an array of things to run, i.e. functions:

let p = funcs.reduce((p, func) => p.then(() => func()), Promise.resolve());

or an array of values to run a function over:

let p = values.reduce((p, val) => p.then(() => loadValue(val)), Promise.resolve());

Read up on reduce here.

Update: Why Promises don't "run" code.

Most people intuitively understand that callbacks don't run in parallel.

(Workers aside,) JavaScript is inherently event-driven and single-threaded, and never runs in parallel. Only browser functions, e.g. fetch(url) can truly do work in parallel, so an "asynchronous operation" is a euphemism for a synchronous function call that returns immediately, but is given a callback (e.g. where resolve would be called) that will be called later.

Promises don't change this reality. They hold no inherent asynchronous powers (*), beyond what can be done with callbacks. At their most basic, they're a (very) neat trick to reverse the order in which you need to specify callbacks.

*) Technically speaking, promises do have something over callbacks, which is a micro-task queue in most implementations, which just means promises can schedule things at the tail of the current crank of the JavaScript event-loop. But that's still not vastly different, and a detail.

EDIT 2

According to your edit, you're looking for Promise.mapSeries as provided by bluebird. You've given us a bit of a moving target, so this edit changes direction from my previous answer because the mapSeries function works very differently than just executing a collection of Promises in serial order.

// mock bluebird's mapSeries function
// (Promise [a]) -> (a -> b) -> (Promise [b])
Promise.prototype.mapSeries = function mapSeries(f) {
  return this.then(reducek (ys=> x=> k=> {
    let value = f(x);
    let next = x=> k([...ys, x]);
    return value instanceof Promise ? value.then(next) : next(value);
  }) ([]));
};

Just to get a top-level idea of how this would be used

// given: (Promise [a]) and (a -> b)
// return: (Promise [b])
somePromiseOfArray.mapSeries(x=> doSomething(x)); //=> somePromiseOfMappedArray

This relies on a small reducek helper which operates like a normal reduce except that the callback receives an additional continuation argument. The primary advantage here is that our reducing procedure has the option of being asynchronous now. The computation will only proceed when the continuation is applied. This is defined as a separately because it's a useful procedure all on its own; having this logic inside of mapSeries would make it overly complicated.

// reduce continuation helper
// (a -> b -> (a -> a)) -> a-> [b] -> a
const reducek = f=> y=> ([x, ...xs])=> {
  if (x === undefined)
    return y;
  else
    return f (y) (x) (y => reducek (f) (y) (xs));
};

So you can get a basic understanding of how this helper works

// normal reduce
[1,2,3,4].reduce((x,y)=> x+y, 0); //=> 10

// reducek
reducek (x=> y=> next=> next(x+y)) (0) ([1,2,3,4]); //=> 10

Next we have two actions that we'll use in our demos. One that is completely synchronous and one that returns a Promise. This demonstrates that mapSeries can also work on iterated values that are Promises themselves. This is the behaviour defined by bluebird.

// synchronous power
// Number -> Number -> Number
var power = x=> y=> Math.pow(y,x);

// asynchronous power
// Number -> Number -> (Promise Number)
var powerp = x=> y=>
  new Promise((resolve, reject)=>
    setTimeout(() => {
      console.log("computing %d^%d...", y, x);
      if (x < 10)
        resolve(power(x)(y));
      else
        reject(Error("%d is just too big, sorry!", x));
    }, 1000));

Lastly, a small helper used to facilitate logging in the demos

// log promise helper
const logp = p=>
  p.then(
    x=> console.log("Done:", x),
    err=> console.log("Error:", err.message)
  );

Demo time! Here I'm going to dogfood my own implementation of mapSeries to run each demo in sequential order!.

Because mapSeries excepts to be called on a Promise, I kick off each demo with Promise.resolve(someArrayOfValues)

// demos, map each demo to the log
Promise.resolve([

  // fully synchronous actions map/resolve immediately
  ()=> Promise.resolve([power(1), power(2), power(3)]).mapSeries(pow=> pow(2)),

  // asynchronous items will wait for resolve until mapping the next item
  ()=> Promise.resolve([powerp(1), powerp(2), powerp(3)]).mapSeries(pow=> pow(2)),

  // errors bubble up nicely
  ()=> Promise.resolve([powerp(8), powerp(9), powerp(10)]).mapSeries(pow=> pow(2))
])
.mapSeries(demo=> logp(demo()));

Go ahead, run the demo now

// reduce continuation helper
// (a -> b -> (a -> a)) -> a-> [b] -> a
const reducek = f=> y=> ([x, ...xs])=> {
  if (x === undefined)
    return y;
  else
    return f (y) (x) (y => reducek (f) (y) (xs));
};

// mock bluebird's mapSeries function
// (Promise [a]) -> (a -> b) -> (Promise [b])
Promise.prototype.mapSeries = function mapSeries(f) {
  return this.then(reducek (ys=> x=> k=>
    (x=> next=>
      x instanceof Promise ? x.then(next) : next(x)
    ) (f(x)) (x=> k([...ys, x]))
  ) ([]));
};

// synchronous power
// Number -> Number -> Number
var power = x=> y=> Math.pow(y,x);

// asynchronous power
// Number -> Number -> (Promise Number)
var powerp = x=> y=>
  new Promise((resolve, reject)=>
    setTimeout(() => {
      console.log("computing %d^%d...", y, x);
      if (x < 10)
        resolve(power(x)(y));
      else
        reject(Error("%d is just too big, sorry!", x));
    }, 1000));


// log promise helper
const logp = p=>
  p.then(
    x=> console.log("Done:", x),
    err=> console.log("Error:", err.message)
  );

// demos, map each demo to the log
Promise.resolve([

  // fully synchronous actions map/resolve immediately
  ()=> Promise.resolve([power(1), power(2), power(3)]).mapSeries(pow=> pow(2)),

  // asynchronous items will wait for resolve until mapping the next item
  ()=> Promise.resolve([powerp(1), powerp(2), powerp(3)]).mapSeries(pow=> pow(2)),

  // errors bubble up nicely
  ()=> Promise.resolve([powerp(8), powerp(9), powerp(10)]).mapSeries(pow=> pow(2))
])
.mapSeries(f=> logp(f()));


EDIT

I'm reapproaching this problem as a series of promises should be considered like a chain or composition of promises. Each resolve promise will feed it's value to the next promise.

Per @Zhegan's remarks, it makes more sense for the series function to take an array of promise creators, otherwise there's no way to guarantee the promises would run in serial. If you pass an array of Promises, each promise will immediately run its executor and start doing work. Thus, there's no way that the work of Promise 2 could depend on the completed work of Promise 1.

Per @Bergi's remarks, my previous answer was a little weird. I think this update makes things a little more consistent.

Promise series without error

// ([(a-> (Promise b)), (b-> (Promise c)]), ...]) -> a -> (Promise c)
Promise.series = function series(tasks) {
  return x=>
    tasks.reduce((a,b)=> a.then(b), Promise.resolve(x));
};

// a -> [a] -> (Promise [a])
var concatp = x=> xs=>
  new Promise((resolve, reject)=>
    setTimeout(() => {
      console.log(xs, x);
      if (xs.length < 3)
        resolve(xs.concat([x]));
      else
        reject(Error('too many items'));
    }, 250));

var done = (x)=> console.log('done:', x);
var err = (e)=> console.log('error:', e.message);

Promise.series([concatp(3), concatp(6), concatp(9)]) ([]) .then(done, err);
// [] 3
// [ 3 ] 6
// [ 3, 6 ] 9
// done: [ 3, 6, 9 ]

Promise series with an error

// ([(a-> (Promise b)), (b-> (Promise c)]), ...]) -> a -> (Promise c)
Promise.series = function series(tasks) {
  return x=>
    tasks.reduce((a,b)=> a.then(b), Promise.resolve(x));
};

// a -> [a] -> (Promise [a])
var concatp = x=> xs=>
  new Promise((resolve, reject)=>
    setTimeout(() => {
      console.log(xs, x);
      if (xs.length < 3)
        resolve(xs.concat([x]));
      else
        reject(Error('too many items'));
    }, 250));

var done = (x)=> console.log('done:', x);
var err = (e)=> console.log('error:', e.message);

Promise.series([concatp(3), concatp(6), concatp(9), concatp(12)]) ([]) .then(done, err);
// [] 3
// [ 3 ] 6
// [ 3, 6 ] 9
// [ 3, 6, 9 ] 12
// error: too many items

@forrert's answer is pretty much spot on

Array.prototype.reduce is a bit confusing, so here is a version without reduce. Note that in order to actually run promises in series we must wrap each promise in a provider function and only invoke the provider function inside the Promise.series function. Otherwise, if the promises are not wrapped in functions, the promises will all start running immediately and we cannot control the order in which they execute.

Promise.series = function series(providers) {

    const results = [];
    const ret = Promise.resolve(null);

    providers.forEach(function(p, i){
         ret = ret.then(function(){
            return p().then(function(val){
                  results[i] = val;
            });
         });
    });

    return ret.then(function(){
         return results;
    });

}

the equivalent functionality using reduce:

Promise.series = function series(providers) {
    const ret = Promise.resolve(null);
    const results = [];

    return providers.reduce(function(result, provider, index) {
         return result.then(function() {
            return provider().then(function(val) {
               results[index] = val;
            });
         });
    }, ret).then(function() {
        return results;
    });
}

you can test both functions using this:

Promise.series([

    function(){
      return new Promise(function(resolve, reject){
          setTimeout(function(){
              console.log('a is about to be resolved.')
              resolve('a');
          },3000);  
       })   
    },
    function(){
        return new Promise(function(resolve, reject){
            setTimeout(function(){
                  console.log('b is about to be resolved.')
                  resolve('b');
            },1000);
        })
    }   

    ]).then(function(results){
        console.log('results:',results);
    }).catch(function(e){
        console.error('Rejection reason:', e.stack || e);
    });

note that it's not a good idea to attach functions, or otherwise alter, native global variables, like we just did above. However, also note that the native library authors also left us with native libraries that are wanting in functionality :)

发布评论

评论列表(0)

  1. 暂无评论