在开始下一个之前等待上一个承诺的Javascript Map?

Posted

技术标签:

【中文标题】在开始下一个之前等待上一个承诺的Javascript Map?【英文标题】:Javascript Map that waits for previous promise before starting next? 【发布时间】:2017-05-02 21:16:05 【问题描述】:

我知道这不在Array.map 的范围内,但我想等到前一项完成承诺后再开始下一项。碰巧我需要等待上一个条目保存在数据库中才能继续前进。

const statsPromise = stats.map((item) => 
    return playersApi.getOrAddPlayer(item, clubInfo, year); //I need these to wait until previous has finished its promise.
);

Promise.all(statsPromise)
.then((teamData) => 
  ..//
);

playersApi.getOrAddPlayer 返回一个new Promise

编辑

阅读更多内容,显示 playerApi.getOrAddPlayer 似乎很重要

getOrAddPlayer: function (item, clubInfo, year) 
    return new Promise((resolve, reject) => 

        var playerName = item.name.split(' '),
            fname = playerName[0].caps(),
            sname = playerName[1].caps();

                Players.find(
                    fname: fname,
                    sname: sname,
                ).exec()
                .then(function(playerDetails, err)
                    if(err) reject(err);
                    var savePlayer = new Players();
                    //stuff
                    savePlayer.save()
                    .then(function(data, err)
                        if(err)  reject(err);
                        item._id = data._id;
                        resolve(item);
                    );
                );
            );

【问题讨论】:

@FelixKling 我重新打开了这个,因为我觉得你提到的 dup 中的答案不是很好。我知道还有其他更好的副本。如果你能找到他们,请告诉我。 你会原谅我的 cmets 的不可知论,但我并没有真正使用承诺(我知道我应该,但考虑到我不专业地这样做有不是理由),但我可能会考虑做的是将一系列需要完成的项目保留在 promise 函数的范围之外,然后只需 Array.pop()-ing 关闭数据并根据会做的事情创建一个新的 promise同样的事情。 相关:Promise version of a “while” loop? SO 文档中也有Reduce an array to chained promises。 Bluebird's Promise.mapSeries() 会这样做。 【参考方案1】:

您可以使用归约而不是映射来实现此目的:

stats.reduce(
  (chain, item) =>
    // append the promise creating function to the chain
    chain.then(() => playersApi.getOrAddPlayer(item, clubInfo, year)),
  // start the promise chain from a resolved promise
  Promise.resolve()
).then(() => 
  // all finished, one after the other
);

演示:

const timeoutPromise = x => 
  console.log(`starting $x`);
  return new Promise(resolve => setTimeout(() => 
    console.log(`resolving $x`);
    resolve(x);
  , Math.random() * 2000));
;

[1, 2, 3].reduce(
  (chain, item) => chain.then(() => timeoutPromise(item)),
  Promise.resolve()
).then(() =>
  console.log('all finished, one after the other')
);

如果需要累加值,可以通过归约传播结果:

stats
  .reduce(
    (chain, item) =>
      // append the promise creating function to the chain
      chain.then(results =>
        playersApi.getOrAddPlayer(item, clubInfo, year).then(data =>
          // concat each result from the api call into an array
          results.concat(data)
        )
      ),
    // start the promise chain from a resolved promise and results array
    Promise.resolve([])
  )
  .then(results => 
    // all finished, one after the other
    // results array contains the resolved value from each promise
  );

演示:

const timeoutPromise = x => 
  console.log(`starting $x`);
  return new Promise(resolve =>
    setTimeout(() => 
      console.log(`resolving result for $x`);
      resolve(`result for $x`);
    , Math.random() * 2000)
  );
;

function getStuffInOrder(initialStuff) 
  return initialStuff
    .reduce(
      (chain, item) =>
        chain.then(results =>
          timeoutPromise(item).then(data => results.concat(data))
        ),
      Promise.resolve([])
    )


getStuffInOrder([1, 2, 3]).then(console.log);

变体 #1:Array.prototype.concat 看起来更优雅,但会在每个连接上创建一个新数组。为了提高效率,您可以使用 Array.prototype.push 和更多样板:

stats
  .reduce(
    (chain, item) =>
      chain.then(results =>
        playersApi.getOrAddPlayer(item, clubInfo, year).then(data => 
          // push each result from the api call into an array and return the array
          results.push(data);
          return results;
        )
      ),
    Promise.resolve([])
  )
  .then(results => 

  );

演示:

const timeoutPromise = x => 
  console.log(`starting $x`);
  return new Promise(resolve =>
    setTimeout(() => 
      console.log(`resolving result for $x`);
      resolve(`result for $x`);
    , Math.random() * 2000)
  );
;

function getStuffInOrder(initialStuff) 
  return initialStuff
    .reduce(
      (chain, item) =>
        chain.then(results =>
          timeoutPromise(item).then(data => 
            results.push(data);
            return results;
          )
        ),
      Promise.resolve([])
    );


getStuffInOrder([1, 2, 3]).then(console.log);

变体 #2:您可以将 results 变量提升到上限范围。这将消除嵌套函数的需要,以便在累积数据时通过最近的闭包使results 可用,而是使其对整个链全局可用。

const results = [];
stats
  .reduce(
    (chain, item) =>
      chain
        .then(() => playersApi.getOrAddPlayer(item, clubInfo, year))
        .then(data => 
          // push each result from the api call into the globally available results array
          results.push(data);
        ),
    Promise.resolve()
  )
  .then(() => 
    // use results here
  );

演示:

const timeoutPromise = x => 
  console.log(`starting $x`);
  return new Promise(resolve =>
    setTimeout(() => 
      console.log(`resolving result for $x`);
      resolve(`result for $x`);
    , Math.random() * 2000)
  );
;

function getStuffInOrder(initialStuff) 
  const results = [];
  return initialStuff.reduce(
    (chain, item) =>
      chain
        .then(() => timeoutPromise(item))
        .then(data => 
          results.push(data);
          return results;
        ),
    Promise.resolve()
  );


getStuffInOrder([1, 2, 3]).then(console.log);

【讨论】:

好朋友 :) 我必须说,我没有得到 `year)), // 从已解决的承诺开始承诺链 Promise.resolve()` 逗号在这里做?我们在每个循环中返回承诺链。我只是不明白该功能的设置方式, 同样在这个例子中,我们没有将 playerApi 返回的值传递给 then 逗号分隔传递给stats.reduce 的两个参数。第一个参数是一个函数,它接受 Promise 链和当前项并进一步构建链,第二个参数是一个空的已解析 Promise,从它开始链。 就累积值而言,有两种方法。一种是在外部范围内创建一个数组,并在每次 Promise 解析时推送结果,(chain, item) => chain.then(() => playersApi.getOrAddPlayer(item, clubInfo, year)).then((result) => outerArray.push(result));。另一种是通过递归传播结果,如guest271314's answer 所示,或者通过每次调用reduce 或每次调用playersApi.getOrAddPlayer 传递结果。我将添加一个示例。 我喜欢这种方法……太聪明了!谢谢!【参考方案2】:

如果您对使用 Promise 库感到满意,您可以在这种情况下使用 Promise.mapSeries by Bluebird。

例子:

const Promise = require("bluebird");
//iterate over the array serially, in-order
Promise.mapSeries(stats, (item) => 
  return playersApi.getOrAddPlayer(item, clubInfo, year));
).then((teamData) => 
  ..//
);

【讨论】:

【参考方案3】:

您可以使用递归解决方案

const statsPromise = (function s(p, results) 
  return p.length ? playersApi.getOrAddPlayer(p.shift(), clubInfo, year) : results;
)(stats.slice(0), []);

statsPromise
.then((teamData) => 
//do stuff
);

let n = 0;
let promise = () => new Promise(resolve => 
                setTimeout(resolve.bind(null, n++), 1000 * 1 + Math.random()));

let stats = [promise, promise, promise];

const statsPromise = (function s(p, results) 
  return p.length ? p.shift().call().then(result => 
    console.log(result);
    return s(p, [...results, result])
  ) : results;
)(stats.slice(0), []);
    
statsPromise.then(res => console.log(res))

【讨论】:

【参考方案4】:

你可以使用一种递归:

function doStats([head, ...tail]) 
  return !head ? Promise.resolve() :
    playersApi.getOrAddPlayer(head, clubInfo, year)
      .then(() => doStats(tail));


doStats(stats)
  .then(() => console.log("all done"), e => console.log("something failed", e));

另一个经典的方法是使用reduce

function doStats(items) 
  return items.reduce(
    (promise, item) => 
      promise.then(() => playersApi.getOrAddPlayer(item, clubInfo, year)),
    Promise.resolve());

顺便说一句,你可以清理你的 getOrAddPlayer 函数,并避免使用 Promise 构造函数反模式:

getOrAddPlayer: function (item, clubInfo, year) 
    var playerName = item.name.split(' '),
        fname = playerName[0].caps(),
        sname = playerName[1].caps();

    return Players.find(fname, sname).exec()
      .then(playerDetails => new Players().save())
      .then(_id => Object.assign(item, _id));

【讨论】:

Hot dam,这是一些干净的 JS。我对整个 Promise 链都很陌生 :) 对您的第一个函数很感兴趣,您可以使用 Promise.resolve() 作为完成时间的标记。 在清理GetOrAddPlayer我相信你必须回报一个承诺。我没有意识到你可以从 Mongoose 中返回整个承诺链? @JamieHutber 没错。你已经有了从猫鼬那里得到的承诺;你不需要将它包装在另一个承诺中。那是explicit promise constructor anti-pattern。 对您的第一个函数很感兴趣,您可以使用Promise.resolve() 作为完成时间的标记。 对。这对应于如果你给它喂零个项目,你就成功了。 我从来没有想过要像这样使用Promise.resolve。我一直把它想象成一种方式.,.. 好吧实际上是的,但我没有像这样应用它:D 现在我只需要返回在playersApi.getOrAddPlayer 中收集的数据,然后我们就可以了 :) 【参考方案5】:

我考虑了一下,但没有找到比 reduce 更好的方法。

适应你的情况,它会是这样的:

const players = [];
const lastPromise = stats.reduce((promise, item) => 
  return promise.then(playerInfo => 
    // first iteration will be undefined
    if (playerInfo) 
       players.push(playerInfo)
    
    return playersApi.getOrAddPlayer(item,  clubInfo, year);
  );
, Promise.resolve());

// assigned last promise to a variable in order to make it easier to understand
lastPromise.then(lastPlayer => players.push(lastPlayer));

你可以看到一些关于这个here的解释。

【讨论】:

以上是关于在开始下一个之前等待上一个承诺的Javascript Map?的主要内容,如果未能解决你的问题,请参考以下文章

外部函数调用,承诺,异步和Mongo - 混淆

与条件承诺链反应并在发送调度之前等待它们

使 angular.forEach 在转到下一个对象后等待承诺

在 Chrome 扩展程序中,如何确保在使用 chrome-promise 的下一个承诺之前解决上一个承诺?

在映射下一项之前,异步等待映射不等待异步函数在映射函数内部完成

Mocha 在运行测试之前似乎没有等待承诺链完成