等待 Promise 返回的第一个 true 的干净方式

Posted

技术标签:

【中文标题】等待 Promise 返回的第一个 true 的干净方式【英文标题】:Clean way to wait for first true returned by Promise 【发布时间】:2018-12-12 02:28:54 【问题描述】:

我目前正在做一些事情,我在一个数组中发出三个 Promise。目前它看起来像这样

var a = await Promise.all([Promise1(), Promise2(), Promise3()]);

现在所有这些承诺要么返回真要么假。但目前我正在等待所有这些都完成,一旦其中一个返回 true,我就可以继续。

我想了一些方法来实现这一点,但似乎都很难看。你将如何解决这个任务?

【问题讨论】:

你不能简单地使用Promise.race吗? @user753642 我不这么认为。 Race 意味着,总是在一个 Promise 完成后停止。只有当任何一个 Promise 是 true 时,OP 才想停止。至少,这是我的理解。 正确。感谢您的快速和许多答案。我想我得解决这个问题。看起来很有希望 作为对此的更新,Promise.any() 可能会成为您想要的,一旦它到达常青浏览器。 【参考方案1】:

您可以创建一个new promise,只要任何给定的承诺解析为true,它就会立即解析,如下所示:

function promiseRaceTrue(promises) 
    return new Promise(function(resolve, reject) 
        promises.forEach(promise =>
            promise.then(val => val === true && resolve())
            // TODO handle resolve with value of "false"?
            // TODO handle rejection?
        );
        // TODO handle all resolved as "false"?
    );


var a = await promiseRaceTrue([Promise1(), Promise2(), Promise3()]);

它的行为类似于 Promise.race,但仅在给定的 Promise 之一以值 true 解析时才解析,而不是在任何给定的 Promise 解析或拒绝时立即解析。

【讨论】:

如果他们都返回false会发生什么?承诺永远不会解决吗? 使用当前代码,不,它不会解决。但是你可以通过添加Promise.all 轻松解决这个问题,如果所有给定的承诺都解析为false,则可以解决或拒绝。此外,您可能需要处理拒绝,但问题中未指定。 我相信唯一的其他“干净”方法可能是将这个函数添加到Promise.prototype;但是,这可能是不干净的,因为它会编辑原型。 @Tyler 是的,“干净的方式”和“将此功能添加到Promise.prototype”在我看来是矛盾的;)【参考方案2】:

可以使用Promise.race(),并且只有在值为真时才解析初始承诺

const doSomething = (bool, val)=>
  return new Promise((resolve, reject)=>
     if (bool)
        resolve(val)
     
  )


const promise1 = doSomething(false,'one')
const promise2 = doSomething(false,'two')
const promise3 = doSomething(true,'three')
const promise4 = doSomething(true,'four')
const promise5 = doSomething(true,'five')

Promise.race([promise1, promise2, promise3, promise4, promise5]).then(value => 
  console.log('First true one to resolve is: ', value);
  
);

【讨论】:

【参考方案3】:

您可以结合Promise.racePromise.all 来实现:

function firstTrue(promises) 
    const newPromises = promises.map(p => new Promise(
        (resolve, reject) => p.then(v => v && resolve(true), reject)
    ));
    newPromises.push(Promise.all(promises).then(() => false));
    return Promise.race(newPromises);

测试上述代码:

function firstTrue(promises) 
  const newPromises = promises.map(p => new Promise(
    (resolve, reject) => p.then(v => v && resolve(true), reject)
  ));
  newPromises.push(Promise.all(promises).then(() => false));
  return Promise.race(newPromises);


var test = values => firstTrue(
  values.map((v) => new Promise((resolve) => 
    setTimeout(() => resolve(v), Math.round(Math.random() * 1000));
  ))
).then((ret) => console.log(values, ret));

test([true, true, true]);
test([false, false, false]);
test([true, false, false]);
test([false, true, false]);
test([false, false, true]);

【讨论】:

这实际上可以解决如果所有这些都解析为 false 会发生什么的问题。 我认为这是最苗条的方式。接受答案?你怎么看? 这里没有竞争条件吗?如果所有的 Promise 同时解析为 true(或者在调用 firstTrue() 之前已经解析为 true),那么单个转换后的 Promise 不会与 .all() Promise 竞争吗?我认为用这个替换 .all() 承诺会解决它:Promise.all(promises).then(values => values.some(x => !!x)) @RichardHansen 的 Promise.then 分辨率在内部排队(此行为是 EcmaScript 标准所要求的)。 Promise.all 在内部调用 Promise.then 传递给它的承诺。然后需要在 Promise.all 返回的 Promise 上调用一个额外的 Promise.then。因此,Promise.all 的解析至少需要两个入队操作,而数组中的其他 Promise 只需要一个(p.then)。 ES 要求这些入队操作按照它们被调度的顺序被调用。所以最终的解析总是会在 Promise.all 的解析之前被调度。【参考方案4】:

如果您想要最先进的解决方案,听起来您想要Promise.any() 哪个:

Promise.any() 采用 Promise 对象的可迭代对象,并且只要 可迭代中的一个promise fulfils,返回一个promise 用该承诺的价值解决。如果没有承诺 可迭代的完成(如果所有给定的承诺都被拒绝),那么 返回的承诺被 AggregateError 拒绝,一个新的子类 Error 将单个错误组合在一起。本质上,这 方法与Promise.all()相反。

但是,直到

铬 85 火狐79 Safari 14 不支持 IE、Edge 或 Opera

你基本上想要some()

Promise.all() 不起作用,因为它会让你等待所有 Promise 完成。 Promise.race() 单独不起作用,因为它只返回 1 Promise 的分辨率。

相反,我们需要接收一个 Promise 数组并继续比赛,直到其中一个返回 true,此时我们应该停止。如果它们都不是真的,我们仍然需要停下来,但我们需要注意它们都不是真的。

考虑以下带有测试工具的示例:

代码

/**
 * Promise aware setTimeout based on Angulars method
 * @param Number delay How long to wait before resolving
 * @returns Promise A resolved Promise to signal the timeout is complete
 */
function $timeout(delay) 
    return new Promise((resolve, reject) => 
        setTimeout(() => resolve(), delay);
    );


/**
 * Return true (early) if any of the provided Promises are true
 * @param Function(arg: *): Boolean predicate The test the resolved Promise value must pass to be considered true
 * @param Promise[] arr The Promises to wait on
 * @returns Promise<Boolean> Whether one of the the provided Promises passed the predicate
 */
async function some(predicate, arr) 
    // Don't mutate arguemnts
    const arrCopy = arr.slice(0);

    // Wait until we run out of Promises
    while(arrCopy.length)
        // Give all our promises IDs so that we can remove them when they are done
        const arrWithIDs = arrCopy.map((p, idx) => p.then(data => (idx, data)).catch(_err => (idx, data: false)));
        // Wait for one of the Promises to resolve
        const soon = await Promise.race(arrWithIDs);
        // If it passes the test, we're done
        if(predicate(soon.data))return true;
        // Otherwise, remove that Promise and race again
        arrCopy.splice(soon.idx, 1);
    
    // No Promises passed the test
    return false;


// Test harness
const tests = [
    function allTrue()
        console.log(new Date());
        return some((v)=>v, [
            $timeout(1000).then(() => true),
            $timeout(2000).then(() => true),
            $timeout(3000).then(() => true)
        ]).then(d => 
            console.log(d);
            console.log(new Date());
        );
    ,
    function twoSecondsTrue()
        console.log(new Date());
        return some((v)=>v, [
            $timeout(1000).then(() => false),
            $timeout(2000).then(() => true),
            $timeout(3000).then(() => true)
        ]).then(d => 
            console.log(d);
            console.log(new Date());
        );
    ,
    function threeSecondsTrue()
        console.log(new Date());
        return some((v)=>v, [
            $timeout(1000).then(() => false),
            $timeout(2000).then(() => false),
            $timeout(3000).then(() => true)
        ]).then(d => 
            console.log(d);
            console.log(new Date());
        );
    ,
    function allFalse()
        console.log(new Date());
        return some((v)=>v, [
            $timeout(1000).then(() => false),
            $timeout(2000).then(() => false),
            $timeout(3000).then(() => false)
        ]).then(d => 
            console.log(d);
            console.log(new Date());
        );
    ,
    function threeSecondsTrueWithError()
        console.log(new Date());
        return some((v)=>v, [
            $timeout(1000).then(() =>  throw new Error() ),
            $timeout(2000).then(() => false),
            $timeout(3000).then(() => true)
        ]).then(d => 
            console.log(d);
            console.log(new Date());
        );
    
]

tests.reduce((acc, curr) => acc.then(()=>curr()), Promise.resolve());

输出

// 1 Second true
2018-07-03T18:41:33.264Z
true
2018-07-03T18:41:34.272Z

// 2 Seconds true
2018-07-03T18:41:34.273Z
true
2018-07-03T18:41:36.274Z

// 3 Seconds true
2018-07-03T18:41:36.274Z
true
2018-07-03T18:41:39.277Z

// 3 Seconds false
2018-07-03T18:41:39.277Z
false
2018-07-03T18:41:42.282Z

// 3 Seconds true with error throwing
2018-07-03T18:41:42.282Z
true
2018-07-03T18:41:45.285Z

【讨论】:

你是承诺的忍者【参考方案5】:

根据 str 的回答,我想添加提供回调的功能,这样它就不仅仅适用于 true/false Promises。

添加回调将允许测试任何 Promise 的结果,并返回成功匹配回调过滤器的 Promise(应返回 true/false 值)。

Promise.until = function(callback, ...promises) 
  return new Promise(function(resolve, reject) 
    promises.forEach(promise =>
      promise.then(val => callback(val) === true && resolve(val))
    )
  )


// Create some functions that resolve true/false
function Promise1() return new Promise(resolve => setTimeout(()=> resolve(false), 1000))
function Promise2() return new Promise(resolve => setTimeout(()=> resolve(true), 3000))
function Promise3() return new Promise(resolve => setTimeout(()=> resolve(false), 1000))

// Create som functions that resolve objects
function Promise4() return new Promise(resolve => setTimeout(()=> resolve(a:1), 1000))
function Promise5() return new Promise(resolve => setTimeout(()=> resolve(a:2), 3000))
function Promise6() return new Promise(resolve => setTimeout(()=> resolve(a:123), 1000))

// Create some functions that resolve strings
function Promise7() return new Promise(resolve => setTimeout(()=> resolve('Brass'), 1000))
function Promise8() return new Promise(resolve => setTimeout(()=> resolve('Monkey'), 500))
function Promise9() return new Promise(resolve => setTimeout(()=> resolve(['Brass', 'Monkey']), 100))

// Once one resolves `true` we will catch it
Promise.until(result => result === true, Promise1(), Promise2(), Promise3())
  .then(result => console.log(result));

// Once one resolves where `a` equals 123, we will catch it
Promise.until(result => result.a === 123, Promise4(), Promise5(), Promise6())
  .then(result => console.log(result));
  
// Once one resolves where `a` equals 123, we will catch it
Promise.until(result => result === 'Monkey', Promise7(), Promise8(), Promise9())
  .then(result => console.log(result));

【讨论】:

【参考方案6】:

好的,我将保留已接受的答案。但我根据自己的需要对其进行了一些修改,因为我认为这个更容易阅读和理解

const firstTrue = Promises => 
    return new Promise((resolve, reject) => 
        // map each promise. if one resolves to true resolve the returned promise immidately with true
        Promises.map(p => 
            p.then(result => 
                if(result === true)
                    resolve(true);
                    return;
                 
            );
        );
        // If all promises are resolved and none of it resolved as true, resolve the returned promise with false
        Promise.all(Promises).then(() => 
            resolve(Promises.indexOf(true) !== -1);
        );   
    );    
 

【讨论】:

以上是关于等待 Promise 返回的第一个 true 的干净方式的主要内容,如果未能解决你的问题,请参考以下文章

返回一个promise而不等待nodejs中函数中的依赖promise

让 promise 在返回前等待几秒钟

TypeScript - 等待 observable/promise 完成,然后返回 observable

在返回函数的变量之前,如何等待 promise 完成?

当我从等待移动到 Promise.all 时,TypeScript 函数返回类型错误

如何在做其他事情之前返回许多 Promise 并等待它们