如何知道所有 Promise 何时在动态“可迭代”参数中得到解决?

Posted

技术标签:

【中文标题】如何知道所有 Promise 何时在动态“可迭代”参数中得到解决?【英文标题】:How to know when all Promises are Resolved in a dynamic "iterable" parameter? 【发布时间】:2016-10-14 13:47:23 【问题描述】:

我的问题是我不知道如何知道动态承诺数组何时解决了所有承诺。

这里是一个例子:

var promiseArray = [];
promiseArray.push(new Promise()/*blablabla*/);
promiseArray.push(new Promise()/*blablabla*/);
Promise.all(promiseArray).then(function()
    // This will be executen when those 2 promises are solved.
);
promiseArray.push(new Promise()/*blablabla*/);

我这里有问题。 Promise.all 行为将在前 2 个 Promise 解决时执行,但是在这 2 个 Promise 解决之前,添加第三个 Promise 并且不会考虑这个新的 Promise。

所以,我需要的是这样说:“嘿Promise.all,你有一个动态数组要检查”。我该怎么做?

请记住,这只是一个示例。我知道我可以将行 Promise.all 移动到最后一行,但实际上新的 Promise 是在解决另一个 Promise 时动态添加的,并且新的 Promise 也可以添加新的 Promise,所以它是一个真正的动态数组。

我的真实用例是这样的:

    我使用 Twitter API 检查是否有新推文(使用 Search Api)。 如果我发现新的推文,我会将其添加到 MongoDB(这里我们有 Promises)。 如果这些新推文与我的 MongoDB 中没有的用户相关(这里我们有新的承诺,因为我必须去 MongoDB 检查我是否有那个用户),我们去 Twitter API获取用户信息(更多承诺),然后我们将这些新用户添加到 MongoDB(是的,更多承诺)。 然后,我去 MongoDB 插入新值以将新推文与这些新用户相关联(更多承诺!wiii!)。 当对 MongoDB 的所有查询都已解决(所有选择、更新、插入)时,关闭 MongoDB 连接。

另一个困难的例子:

var allPromises = [];

allPromises.push(new Promise(function(done, fail)
    mongoDB.connect(function(error)
        //Because mongoDB works with callbacks instead of promises
        if(error)
            fail();
        else
            ajax.get('/whatever').then(function()
                if (somethingHappens) 
                    allPromises.push(new Promise(function(done, fail) //This promise never will be take in account
                        // bla bla bla
                        if (somethingHappens) 
                            allPromises.push(new Promise(function(done, fail) //This promise never will be take in account
                                // bla bla bla
                            ));
                         else 
                            ajax.get('/whatever/2').then(function()
                                if (somethingHappens) 
                                    allPromises.push(new Promise(function(done, fail) //This promise never will be take in account
                                        // bla bla bla
                                    ));
                                
                            );
                        
                    ));
                 else 
                    ajax.get('/whatever/2').then(function()
                        if (somethingHappens) 
                            allPromises.push(new Promise(function(done, fail) //This promise never will be take in account
                                // bla bla bla
                                    if (somethingHappens) 
                                        allPromises.push(new Promise(function(done, fail) //This promise never will be take in account
                                            // bla bla bla
                                        ));
                                     else 
                                        ajax.get('/whatever/2').then(function()
                                            if (somethingHappens) 
                                                allPromises.push(new Promise(function(done, fail) //This promise never will be take in account
                                                    // bla bla bla
                                                ));
                                            
                                        );
                                    
                            ));
                        
                    );
                
            );
    );
));

Promise.all(allPromises).then(function()
    // Soooo, all work is done!
    mongodb.close()!
);

所以,现在,一个美丽的例子。当调用最后一个(我们不知道哪个是最后一个)promise 时,我们需要调用 showAllTheInformation 函数。你是怎么做到的?:

var name = 'anonimus';
var date = 'we do not know';

function userClikOnLogIn() 
    $http.get('/login/user/password').then(function(data)
        if (data.logguedOk) 
            $http.get('/checkIfIsAdmin').then(function(data)
                if (data.yesHeIsAnAdmin) 
                    $http.get('/getTheNameOfTheUser').then(function(data)
                        if(data.userHasName) 
                            $http.get('/getCurrentDate').then(function(data)
                                currentDate = data.theNewCurrentDate;
                            );
                        
                    );
                
            );
        
    );


function showAllTheInformation() 
    alert('Hi ' + name + ' today is:' + date);

这里是另一个具有更多上下文的示例: https://jsfiddle.net/f0a1s79o/2/

【问题讨论】:

动态添加 Promise 的用例是什么? @Bergi 我刚刚添加了有关用例的更多信息 使用问题中描述的步骤 1-5 有什么问题? @guest271314 因为我有 300 个 forEach 添加 300 个承诺,当它们解决时可能会再添加 300 个承诺或可能不添加。所以你不知道谁以及何时添加了数组中的最后一个承诺。所以你不知道什么时候应该调用 Promise.all @NoelBroda 无论是一、二还是三百个承诺都无关紧要。如果流程需要五个步骤,则在第 5 步完成时将 Promise 返回到 Promise.all() 内的可迭代对象。 【参考方案1】:

您可以制作一个简洁的小递归函数来包装 Promise.all 以处理对原始承诺的添加

/**
 * Returns a Promise that resolves to an array of inputs, like Promise.all.
 *
 * If additional unresolved promises are added to the passed-in iterable or
 * array, the returned Promise will additionally wait for those, as long as
 * they are added before the final promise in the iterable can resolve.
 */
function iterablePromise(iterable) 
  return Promise.all(iterable).then(function(resolvedIterable) 
    if (iterable.length != resolvedIterable.length) 
      // The list of promises or values changed. Return a new Promise.
      // The original promise won't resolve until the new one does.
      return iterablePromise(iterable);
    
    // The list of promises or values stayed the same.
    // Return results immediately.
    return resolvedIterable;
  );


/* Test harness below */

function timeoutPromise(string, timeoutMs) 
  console.log("Promise created: " + string + " - " + timeoutMs + "ms");
  return new Promise(function(resolve, reject) 
    window.setTimeout(function() 
      console.log("Promise resolved: " + string + " - " + timeoutMs + "ms");
      resolve();
    , timeoutMs);
  );


var list = [timeoutPromise('original', 1000)];
timeoutPromise('list adder', 200).then(function() 
  list.push(timeoutPromise('newly created promise', 2000));
);
iterablePromise(list).then(function()  console.log("All done!"); );

请记住,这仅涵盖加法,并且仍然有点危险:您需要确保回调顺序使得任何正在进行的承诺都将自己添加到列表中之前 @可以调用987654323@回调。

【讨论】:

哇,终于合理地实现了一个功能,完成了一个很好的解释!比所有删除的答案加起来要好得多:-) 只想说我终于开始实施这个了,而且效果很好。我发现这比其他可用的解决方案更直观。 太棒了。为什么这不是公认的答案? WTF 对人有问题? 这对于递归函数非常有用,因为递归函数可以添加额外的承诺。您可以在解决基本情况之前添加到迭代。谢谢! @ChristophThiede 我不确定你的意思。如果您的意思是延迟或排序,在大多数情况下,异步工作会在 Promise 创建后立即启动,因此此方法不会带来额外的问题;对于像 Google 的 RPC 库这样在开始之前等待调用 then 的异常,您可以手动调用 .then 或再次启动 Promise.alliterablePromise。如果您在谈论 JSDoc 和脚注中的警告,它只是提醒 Promise 是一次性机制:在 iterablePromise 解决之后,它不会尊重您以后添加的任何内容。【参考方案2】:

我知道我在这里聚会迟到了。但是,对于那些感到难过并且没有找到快速答案的人,如果您确定在完成所有操作后不会添加新的承诺,那么这是一种无需重新架构即可继续前进的肮脏(稍后解释)方式现有的承诺。

var promiseArray = [], completedPromises = [];
promiseArray.push(new Promise()/*blablabla1*/);
promiseArray.push(new Promise()/*blablabla2*/);
while(completedPromises.length != promiseArray.length) completedPromises = await Promise.all(promiseArray);

代码中的其他地方(在完成所有先前的承诺之前:

promiseArray.push(new Promise()/*blablabla3*/);

希望这对某人有所帮助。经过多年的免费加载后,我创建了一个堆栈溢出帐户 :)

【讨论】:

【参考方案3】:

没有出路。在调用 Promise.all 之前,您必须将所有承诺放入数组中。在您提供的示例中,这就像将最后一行移到顶部一样简单。

如果您异步填充数组,您应该获得该数组的承诺,并使用.then(Promise.all.bind(Promise))。如果您不知道何时停止添加 Promise,那么这无论如何都是不可能的,因为它们可能永远都不会被解决。


关于你的“美女榜样”,你会想了解magic of chaining。正如我之前在 cmets 中所说的那样,您必须从每个异步执行任何功能的函数中向return 作出承诺。确实,只需添加缺少的returns:

function userClikOnLogIn() 
    return $http.get('/login/user/password').then(function(data)
//  ^^^^^^
        if (data.logguedOk) 
            return $http.get('/checkIfIsAdmin').then(function(data)
//          ^^^^^^
                if (data.yesHeIsAnAdmin) 
                    return $http.get('/getTheNameOfTheUser').then(function(data)
//                  ^^^^^^
                        if(data.userHasName) 
                            return $http.get('/getCurrentDate').then(function(data)
//                          ^^^^^^
                                currentDate = data.theNewCurrentDate;
                            );
                        
                    );
                
            );
        
    );


userClikOnLogIn().then(function showAllTheInformation() 
//               ^^^^^ now you can chain onto it!
    alert('Hi ' + name + ' today is:' + date);
);

这里没有动态增长的承诺数组,只是每个函数都为其所做的事情的(异步)结果返回一个承诺。

【讨论】:

很抱歉,我认为这是一个错误的答案。您建议 OP 重现 the pyramid of doom,这是首先引入承诺的主要原因之一。 @Roque 这个答案提供了正确的结果。要求是在调用最终函数之前完成五个步骤 @guest271314 这并不是因为它可以工作,所以它是一种正确的方法。 Promise 以及如何有效地使用它们,虽然它们是一个非常强大的工具,但经常被误解。以this article 为例。 @Roque:不,你错了。这不是末日的回调金字塔。 Promise 在这里被正确地用作返回值——您看到的金字塔只是嵌套的 if 块,即使没有 Promise,这自然会导致缩进。这正是想要的控制流。当然你也可以使用异常,但是这两种方法都不是“坏”或错误的。 @Bergi 我同意这不是一个回调金字塔,它比大多数金字塔要复杂一些。但我仍然觉得它有点难看,这是可以避免的。我认为这是手动拒绝的完美用例。【参考方案4】:

如果您可以检测 Promise 或其用法,并且范围问题允许这样做,那么我认为您可以更简单地解决问题:有多少 Promise 未完成?

换句话说,您不需要跟踪所有的承诺,只需计算它们。

var outstanding = 0;

var p1 = new Promise()/*blablabla*/;
var p2 = new Promise()/*blablabla*/;

++outstanding;
p1.then( (data) =>  ...
  if (0 >= --outstanding) 
    // All resolved!


// dynamic set of promises, so later we decide to add another:
var p3 = new Promise()/*blablabla*/;
++outstanding;
p3.then( ... );  // as above

要改进上述内容,请将其全部包装到元承诺中(相当于 Promise.all 为一组静态承诺返回的那个)...

  // Create a promise that tracks completion of a dynamic set of instrumented promises.
  getCompletionP()  
    let rslv = null;
    const p = new Promise(
      function(resolve, reject) 
        rslv = resolve;
       );
    p.resolve = rslv;
    assert( p.resolve );
    p.scheduled = 0;
    p.onSchedule = function()  ++this.scheduled; ;
    p.onComplete = function()   if (0 >= --this.scheduled) this.resolve(); ;
    return p;
  

现在在每次调用 then() 之前调用 cp.onSchedule(),并在每个 then() 结束时调用 cp.onComplete,并且 cp 将在你的所有 promise then 函数完成后解析。 (当然,您还需要处理 catch 语句。)

当通过 Promise.then 调用安排的所有代码完成时,这将解决,而问题要求在解决所有承诺后解决的问题。这可以通过在 promise 的 resolve 语句之后添加调用来实现,但如果使用 3rd 方库,这是不可能的,而且我认为它们在功能上是相同的。

这不适用于所有情况,但由于公认的答案是它(动态的承诺集)无法完成,我认为这仍然有用,尽管它变得更复杂(混乱),因为我写出来吧!

【讨论】:

【参考方案5】:

@JeffBowman 和@Bergi 有正确的想法:递归等待和计数承诺。这是我在 Coffeescript 中的实现)

Promise = require 'bluebird'

class DynamicPromiseCollection

    promises = []

    add:(p)->
        promises.push p

    wait_for_all:->
        #
        # Wait for all current promises, then check for new promises...
        # ...if there are new promises, then keep waiting ( recursively ).
        #
        # Resolve only when all promises are done, and there are no new promises.
        #
        make_promise = ->
            num_before = promises.length
            p = Promise.all(promises).then ->
                num_after = promises.length
                if num_after > num_before
                    return make_promise() # recursive -- wait again
                else
                    return true # all done now
        p = make_promise()
        return p


#
# let's test this...
#
promises = new DynamicPromiseCollection()


#
# pretend to get remote data
#
get_remote_data = ->
    new Promise (resolve,reject)->
        setTimeout ->
            resolve "data"
        ,500

#
# get data, wait, then get more data...
#
promises.add get_remote_data().then (data)->
    console.log "got " + data
    promises.add get_remote_data().then (data)->
        console.log "got " + data

#
# this should wait for both data
#
promises.wait_for_all().then ->
    console.log "...and wait_for_all is done."

【讨论】:

以上是关于如何知道所有 Promise 何时在动态“可迭代”参数中得到解决?的主要内容,如果未能解决你的问题,请参考以下文章

如何知道动态创建的 Angular 组件何时可以在 DOM 中请求?

如何确定何时从 Swift 中的集合中下载了所有图像?

在递归方法中如何知道我的所有线程何时完成执行?

HTML - 我如何知道所有帧何时加载?

如何找到可迭代的最大值? [关闭]

何时不使用承诺 [关闭]