实现Promise.series作为Promise.all的替代

时间:2016-06-01 21:07:39

标签: javascript node.js ecmascript-6 es6-promise

我看到Promise.all的这个示例实现 - 并行运行所有承诺 - Implementing Promise.all

请注意,我正在寻找的功能类似于Bluebird的Promise.mapSeries http://bluebirdjs.com/docs/api/mapseries.html

我正在尝试创建Promise.series,我有这个似乎按预期工作(它实际上是完全错误的,不要使用它,看到答案):< / p>

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

然而,这个实现的一个潜在问题是,如果我拒绝一个承诺,它似乎没有抓住它:

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

有没有人知道为什么错误没有被捕获,以及是否有办法通过Promises解决这个问题?

根据评论,我做了这个改变:

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

    });

}

但是这仍然没有按预期工作......

4 个答案:

答案 0 :(得分:3)

then循环中forEach返回的承诺不会处理潜在的错误。

正如@slezica在评论中指出的那样,尝试使用reduce而非forEach,这将所有承诺链接在一起。

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

请记住,承诺在那时已经“正在运行”。如果你真的想要系列地运行你的承诺,你应该调整你的函数并传入一系列返回promises的函数。像这样:

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

答案 1 :(得分:3)

编辑2

根据您的编辑,您正在寻找bluebird提供的Promise.mapSeries。你给了我们一些移动的目标,所以这个编辑改变了我之前的答案的方向,因为mapSeries函数的工作方式与按序列顺序执行Promise集合的工作方式完全不同。

// 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);
  }) ([]));
};

只是为了获得如何使用它的顶级想法

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

这依赖于一个小的reducek帮助器,它的操作类似于普通的reduce,除了回调接收到另一个 continuation 参数。这里的主要优点是我们的还原过程现在可以选择异步。只有在应用延续时才会进行计算。这被定义为单独的,因为它本身就是一个有用的过程;在mapSeries内部使用这种逻辑会使它变得过于复杂。

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

因此,您可以基本了解此帮助程序的工作原理

// 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

接下来,我们将在演示中使用两个操作。一个是完全同步的,一个返回Promise的。这表明mapSeries也可以处理Promises本身的迭代值。这是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));

最后,一个小助手用于方便登录演示

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

演示时间!在这里,我将dogfood我自己的mapSeries实现按顺序运行每个演示!。

因为在承诺上调用mapSeries除外,我用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()));

继续,立即运行演示

// 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()));

修改

我正在重新认识这个问题,因为一系列承诺应该被视为承诺的链条或组合。每一个解决承诺都会将它的价值提供给下一个承诺。

Per @ Zhegan的评论,series函数更有意义地采用一系列promise creators ,否则无法保证promises将以串行方式运行。如果传递一个Promises数组,每个promise将立即运行其执行程序并开始工作。因此,Promise 2的工作无法取决于Promise 1的完成工作。

Per @ Bergi的评论,我之前的回答有点奇怪。我认为此更新使事情更加一致。

承诺系列没有错误

// ([(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 ]

承诺系列错误

// ([(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

答案 2 :(得分:3)

这是对承诺如何运作的常见误解。人们希望那里的顺序等效于并行Promise.all

但是承诺不会运行&#34;代码,它们只是return values one attaches completion callbacks to

一系列承诺,这是什么 Promise.all获取,是一个返回值数组。没有办法&#34;运行&#34;它们按顺序排列,因为没有办法运行&#34;返回值。

Promise.all只给你一个代表很多的承诺。

要按顺序运行,请从要运行的事物开始,即函数:

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

或运行函数的值数组:

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

阅读reduce here

更新:为什么承诺不会运行&#34;代码。

大多数人直观地理解回调并不是并行运行。

(除了工作者)JavaScript本质上是事件驱动的和单线程的,并且永远不会并行运行。仅浏览器功能,例如fetch(url)可以真正并行工作,所以&#34;异步操作&#34;是一个同步函数调用的委婉说法,它立即返回,但是给出了一个回调(例如,将调用resolve的地方),稍后将被称为

承诺不会改变这种现实。 除了可以通过回调做的事情之外,它们没有固有的异步功能(*)。从最基本的角度来看,它们是一个(非常)巧妙的技巧来扭转你需要指定回调的顺序。

*)从技术上讲,承诺有一些回调,这在大多数实现中都是一个微任务队列,这只是意味着承诺可以在当前曲柄的尾部安排事情JavaScript事件循环。但这仍然没有太大的不同,还有一个细节。

答案 3 :(得分:1)

@ forrert的答案非常适合

Array.prototype.reduce有点令人困惑,所以这里有一个没有reduce的版本。请注意,为了实际运行promises,我们必须将每个promise包装在provider函数中,并且只调用Promise.series函数中的provider函数。否则,如果promises未包含在函数中,则promise将立即开始运行,并且我们无法控制它们执行的顺序。

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

}

使用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;
    });
}

您可以使用以下方法测试这两个功能:

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

请注意,附加函数或以其他方式更改本机全局变量并不是一个好主意,就像我们上面所做的那样。但是,请注意,本机库作者还给我们留下了需要功能的本机库:)