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

IT技术 javascript promise ecmascript-6
2021-02-11 17:56:35

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

这里有一个例子:

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行为将在解决前两个Promise时执行,但是,在解决这两个Promise之前,添加了第三个Promise,并且不会考虑新的Promise。

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

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

我真正的用例是这样的:

  1. 我使用 Twitter API 检查是否有新推文(使用 Search Api)。
  2. 如果我发现了新的推文,我会将它添加到 MongoDB(这里我们有 Promises)。
  3. 如果这些新推文与我的 MongoDB 中没有的用户相关(这里我们有新的Promise,因为我必须去 MongoDB 来检查我是否有那个用户),我们去 Twitter API 获取用户信息(更多Promise),我们将这些新用户添加到 MongoDB(是的,更多Promise)。
  4. 然后,我去 MongoDB 插入新值以将新推文与这些新用户相关联(更多Promise!wiii!)。
  5. 当对 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()!
});

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

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/

5个回答

你可以做一个整洁的小递归函数包Promise.all处理补充到原来的Promise:

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

请记住,这仅涵盖除,而且它仍然是一个有点危险:你需要确保回调整理是这样的,在飞行中任何Promise将自己添加到列表中之前Promises.all可调用回调。

只想说我终于开始实施这个了,而且效果很好。我发现这比其他可用的解决方案更直观。
2021-03-18 17:56:35
这对于可以添加额外Promise的递归函数非常有用。您可以在解决基本情况之前添加到可迭代对象。谢谢你!
2021-03-18 17:56:35
@ChristophThiede 我不确定你的意思。如果你指的是延迟或排序,大多数情况下异步工作在 Promise 一创建就开始,所以这个方法不会带来额外的问题;对于像 Google 的 RPC 库这样then在开始之前等待调用的异常,您可以手动调用.then或只是启动Promise.alliterablePromise再次调用如果你在谈论 JSDoc 和脚注中的警告,它只是提醒 Promises 是一次性机制:在iterablePromise解决之后,它不会尊重你以后添加的任何内容。
2021-03-31 17:56:35
杰出的。为什么这不是公认的答案?跆拳道是不是人有问题?
2021-04-01 17:56:35
哇,终于有一个合理的函数实现了,并附有一个很好的解释!比所有已删除的答案加在一起要好得多:-)
2021-04-11 17:56:35

我知道我在这里参加聚会迟到了。但是,对于那些感到悲伤并且没有找到快速答案的人来说,如果您确定在完成所有工作后不会添加新的Promise,这里有一种肮脏的(稍后解释)方式,无需重新构建现有的Promise。

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

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

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

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

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

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


关于你的“美丽的例子”,你会想了解链接魔力正如我之前在评论中所说的那样,您必须对return正在执行任何异步操作的每个函数做出Promise。实际上,只需添加缺少的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);
});

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

我很抱歉,但我认为这是一个糟糕的答案。您建议 OP 重现厄运金字塔,这是最初引入Promise的主要原因之一。
2021-03-16 17:56:35
@Roque 这个答案提供了正确的结果。要求是在调用最终函数之前完成五个步骤
2021-03-22 17:56:35
@guest271314 并不是因为它可以工作,所以它是一种正确的方法。Promise 以及如何有效地使用它们,虽然它们是一个非常强大的工具,但经常被误解。例如,看看这篇文章
2021-03-25 17:56:35
@Bergi 我同意它不是回调金字塔,它比大多数金字塔要复杂一些。但我仍然觉得它有点丑陋,这是可以避免的。我认为这是手动拒绝的完美用例。
2021-04-06 17:56:35
@Roque:不,你错了。这不是厄运的回调金字塔。Promise 在这里被正确用作返回值 - 您看到的金字塔只是嵌套的 if 块,即使没有 Promise,这自然会导致缩进。这正是想要的控制流。当然,您也可以使用异常,但两种方法都不是“坏”或错误的。
2021-04-11 17:56:35

如果您可以检测 promise 或它们的用法,并且范围问题允许这样做,那么我认为您可以更简单地解决这个问题:有多少 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中(相当于 Promise.all 为一组静态Promise返回的Promise)...

  // 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都解决时解决问题。这可以通过在 promise 的 resolve 语句之后添加调用来实现,但如果使用 3rd 方库,这是不可能的,我认为它们在功能上是相同的。

这不适用于所有情况,但由于接受的答案是它(动态Promise集)无法完成,我认为这仍然很有用,尽管在我写出来的时候它变得更加复杂(凌乱)!

@JeffBowman 和 @Bergi 有正确的想法:递归等待和计数Promise。这是我在 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."