【问题标题】:Implementing Promise.series as alternative to Promise.all实现 Promise.series 作为 Promise.all 的替代方案
【发布时间】:2016-10-01 10:04:22
【问题描述】:

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

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

我正在尝试创建 Promise.series,我有这个似乎可以按预期工作(实际上是完全错误的,不要使用它,请参阅答案):

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

    });

}

但这仍然无法按预期工作......

【问题讨论】:

  • 你没有抓住内在的承诺。尝试使用reduce() 而不是forEach,通过then 链接它们
  • 但它不会“运行”承诺。 Promise.series 获得已创建的 Promise,并在创建后立即运行。
  • 不,当你通过new Promise(executor) 创建promise 时,executor 函数在创建时被调用(而且是同步的)。在Promise.series 中,你只能等待承诺的履行/拒绝。
  • @AlexMills 不,then() 不会“开始”承诺。 Promise.all([$.get("foo.html"), $.get("bar.html")])Promise.series([$.get("foo.html"), $.get("bar.html")]) 都会立即并行启动 2 个 AJAX 请求,然后等待它们的结果。如果您希望在前一个请求完成后开始下一个请求,您需要给 Promise.series 一个“promise factory”数组:创建(并因此“启动”)promise 的函数。
  • 我会将函数数组(并将其称为任务)传递给Promise.series 并依次执行它们。传递一系列承诺对我来说没有意义 - 它的工作原理几乎与Promise.all

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


【解决方案1】:

@forrert 的回答非常准确

Array.prototype.reduce 有点混乱,所以这里是一个没有reduce的版本。请注意,为了实际串联运行 Promise,我们必须将每个 Promise 包装在提供程序函数中,并且仅在 Promise.series 函数中调用提供程序函数。否则,如果 promise 没有封装在函数中,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);
    });

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

【讨论】:

  • 但是 Promise N 的工作没有办法依赖于前一个 Promise 完成的工作。我知道这可能不是必需的,但它极大地限制了序列中可以拥有的函数类型。
  • 这可能是真的,但你正在考虑 async.waterfall 类型的功能......但我正在寻找的是 async.mapSeries 功能......我们想要一个映射操作,它发生在系列,而不是串联发生的连锁动作。基本上,就像问题的标题所说,我们想要 Promise.all,但要串联!
  • 我不确定它如何限制您可以拥有的功能类型?你可以拥有任何你想要的函数,它会捕获返回的 promise 的 then 值
  • 但是map reducevar map = (f,xs)=&gt; xs.reduce((ys,x)=&gt; ys.concat([f(x)]), []);。然后map(x=&gt;x*x, [1,2,3]); //=&gt; [1,4,9]
  • 我没有关注,我不确定我是否同意映射是一种减少操作,或者您的意思是什么?
【解决方案2】:

编辑 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 也可以处理本身就是 Promise 的迭代值。这是 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 调用,我以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()));

编辑

我正在重新处理这个问题,因为应将一系列承诺视为承诺链或承诺组合。每个解决承诺都会将其价值提供给下一个承诺。

根据@Zhegan 的评论,series 函数采用一组promisecreators 更有意义,否则无法保证promise 会串行运行。如果你传递了一个 Promises 数组,每个 Promise 都会立即运行它的 executor 并开始工作。因此,Promise 2 的工作不可能依赖于 Promise 1 的已完成工作。

根据@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

【讨论】:

  • 你的实现只产生最后一个promise的结果,而不是所有结果的数组。它还将前一个 Promise 的结果传递给下一个 Promise 提供者,这不是它应该做的。最后,让第一个数组元素是一个 Promise 并且其他元素是 Promise 提供者是相当令人困惑的......
  • 我不认为承诺应该以某种方式收集数组中的数据。相反,数据应该按顺序通过 Promise。如果你想收集数据,那应该反映在任务本身中。 Promise.series 不应该在那里产生影响。
  • 就像Array.prototype.reduce;它不会强制您使用concat,但您可以轻松编写一个确实使用concat 的reducer,如果这是您想要的行为。无论如何,没关系。 Promise.series 不是一个真正的函数,所以我们可以让它随心所欲地运行。这就是我的行为方式。
  • "Promise.series 预计至少有 1 个承诺" 非常奇怪。此外,它目前似乎实际上期望该数组中有一个承诺和零个或多个 functions。为什么不直接使用reducestart 参数?
  • @Bergi 感谢您鼓励我重新思考这个问题。我认为这个编辑可能会好一点。
【解决方案3】:

这是对 Promise 工作原理的常见误解。人们希望有一个与并行Promise.all 等效的顺序。

但 promise 不会“运行”代码,它们只是 return values one attaches completion callbacks to

一组承诺,这是什么 Promise.all 是一个返回值数组。没有办法按顺序“运行”它们,因为没有办法“运行”返回值。

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

阅读减少here

更新:为什么 Promises 不能“运行”代码。

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

(撇开工人不谈,)JavaScript 本质上是事件驱动和单线程的,并且从不并行运行。只有浏览器功能,例如fetch(url) 可以真正并行工作,因此“异步操作”是同步函数调用的委婉说法,该函数调用立即返回,但会给出一个回调(例如,resolve 将被调用)将被调用 稍后

Promise 不会改变这个现实。它们没有固有的异步能力 (*),除了回调可以做的。在最基本的情况下,它们是一种(非常)巧妙的技巧,可以颠倒您需要指定回调的顺序。

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

【讨论】:

  • 您应该查看其他答案并相应地调整您的答案
  • 如果承诺“不运行代码”,那么你能解释一下带有签名 function(resolve,reject){} 的函数是什么传递给 Promise 构造函数的吗? :)
  • @AlexMills 那是 Promise 构造函数执行函数。它在你得到承诺之前运行并完成。
  • 回答我自己的问题,该函数实际上是“运行代码”并解析和拒绝用作回调。因此,通过“串行或并行运行 Promise”,此函数中定义的代码实际上将串行或并行运行。
  • @jip,不,不一定,如果您进行一些需要一两秒钟的异步调用,它不一定会被解决/拒绝,此外,如果您将承诺包装在提供程序函数中(正如你所建议的)他们根本不会开始执行。
【解决方案4】:

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

正如@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;
    });
}

请记住,此时 Promise 已经“运行”了。如果你真的想连续运行你的 Promise,你应该调整你的函数并传入一个返回 Promise 的函数数组。像这样的:

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

【讨论】:

  • 你能说得清楚一点,为什么 reduce 比 forEach 更好吗?
  • 当您调用 promise.then 时,将返回一个新的承诺。在您的原始版本中,这个新的承诺在forEach 循环中“丢失”了。使用 reduce 时,您通过数组“链接”初始承诺。
  • 请记住@zhegan 的 cmets。当您在数组中的函数中传递它们时,它们已经“运行”了。如果你真的想要运行一系列的 Promise,你应该传入一个函数数组,一旦前一个 Promise 被解决,你就可以创建你的 Promise。
  • 调用.then()应该会影响 Promise 的执行。任何这样做的实现都是不合规格的;不应依赖此行为。
  • 这会并行运行所有的 Promise,因为链总是以 Promise.resolve(null).then(promise[0]).then... 开始因为第一个 Promise 是 resolve,后续的 .then 调用不会等待任何 Promise下线。如果您尝试运行我的代码但将 Promise.series 函数替换为您的代码,错误将很明显。
猜你喜欢
  • 2012-04-24
  • 1970-01-01
  • 2017-09-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-10-26
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多