通过已解决的 Promise 跳出 for(ever) 循环

Break out of for(ever) loop by resolved Promise

我希望这是一个简单的问题,但我目前无法理解。

我想要做的是跳出一个 while 循环,该循环包含承诺得到解决时的延迟。

在伪代码中,这看起来像:

while( ! promise.resolved ){
    doSomthing()
    await sleep( 5min )
}

循环 必须在 promise 解决后立即中断 而不是等待 sleep 完成。

sleep 目前由 setTimeout 简单地实现,但可以以不同的方式实现。

我希望在 awaited promise 和 sleep 之间有某种空间分离,以更清楚地显示它的工作*)(因为我希望有一个优雅的解决方案可供学习)。 那么但我不喜欢的方法是:

while( true ){

    doSomething()
    try {
        await Promise.race([promise,rejectAfter(5000)])
        break
    } catch( e ){}
} 

如果你必须知道:

doSomething正在发送状态信息。

promise 正在等待用户交互。

*) 此代码的部分目的是 show/demonstrate 其他人希望事情如何运作。所以我正在寻找这个实现级别上最清晰的解决方案。

对您的想法进行细微修改以使其更好地发挥作用:

const sleep = ms =>
    new Promise(resolve => setTimeout(resolve, ms));

async function waitAndDo(promise) {
  let resolved = false;
  const waitFor = promise
    .then((result) => resolved = true);
    
  while(!resolved) {
    doSomething();
    await Promise.race([waitFor, sleep(5000)]);
  }
}
  1. 该函数接受承诺并将一直工作直到它解决。
  2. waitFor 承诺将在 promise 完成并且 resolved 更新为 true 后完成。
  3. 然后 while 循环可以循环直到 resolved 变量设置为 true。在这种情况下,循环将结束并在它之后继续执行。
  4. 在循环内,Promise.race() 将确保它会在 promise 解析 睡眠到期后立即停止等待。以先到者为准。

因此,一旦承诺得到解决,.then() 处理程序就会首先触发并更新 resolvedawait Promise.race(); 将结束等待,并且 while 循环不会再次执行,因为 resolved 现在是 true.

似乎你会使用某种间隔并在 promise 完成时杀死它。

const updateMessage = (fnc, ms, runInit) => {
  if (runInit) fnc();
  const timer = window.setInterval(fnc, ms);
  return function () {
    console.log('killed');
    timer && window.clearTimeout(timer);
  }
}

const updateTime = () => {
  document.getElementById("out").textContent = Date.now();
}

const updateEnd = updateMessage(updateTime, 100, true);

new Promise((resolve) => {
  window.setTimeout(resolve, Math.floor(Math.random()*5000));
}).then(updateEnd);
<div id="out"></div>

作为 VLAZ 的替代品非常合理 , you can avoid the separate boolean sentinel by having your sleep function return some kind of unique sentinel return value that indicates the the timeout. Symbol 正是这种用例的轻量级、独特的对象。

function sleepPromise(ms, resolveWith) {
  return new Promise(resolve => {
    setTimeout(resolve, ms, resolveWith);
  });
}

const inputPromise = new Promise(
    resolve => document.getElementById("wakeUp").addEventListener("click", resolve));

async function yourFunction() {
  const keepSleeping = Symbol("keep sleeping");
  do {
    /* loop starts here */
    console.log("Sleeping...");
    /* loop ends here */
  } while (await Promise.race([inputPromise, sleepPromise(3000, keepSleeping)]) === keepSleeping);
  console.log("Awake!");
}

yourFunction();
<button id="wakeUp">Wake up</button>

一种方法是等待承诺结果,假设它是一个真值1:

const promise = someTask();
let result = undefined;
while (!result) {
    doSomething();
    result = await Promise.race([
        promise, // resolves to an object
        sleep(5000), // resolves to undefined
    ]);
}

1:如果不是,则将 .then(_ => true) 链接到 promise,或者使 sleep 满足您可以区分的特殊值来自 someTask 可能 return 的一切(就像杰夫回答中的符号)。

也可以很好地与 do-while 循环一起使用,因为 result 一开始总是未定义的:

const promise = someTask();
let result = undefined;
do {
    doSomething();
    result = await Promise.race([
        promise, // resolves to an object
        sleep(5000), // resolves to undefined
    ]);
} while (!result);

这里的一个缺点是,如果 doSomething() 抛出异常,则永远不会等待承诺,并且可能会在任务出错时导致未处理的拒绝崩溃。


另一种方法是不使用循环,而是使用 old-school 间隔:

const i = setInterval(doSomething, 5000);
let result;
try {
    result = await someTask();
} finally {
    clearInterval(i);
}

这里的缺点是 doSomething 不会立即调用,而是在 5 秒后才第一次调用。此外,如果 doSomething 抛出异常,它会立即使应用程序崩溃。如果您不希望 doSomething 抛出(或处理 setInterval 回调中的每个异常并期望“循环”继续进行),它仍然可能是一个好方法。


转发来自 someTask()doSomething() 的所有异常的“正确”方法可能如下所示:

let done = false;
const result = await Promise.race([
    (async() => {
        while (!done) {
            doSomething();
            await sleep(5000)
        }
    })(),
    someTask().finally(() => {
        done = true;
    }),
]);

(除了 .finally(),您还可以将 Promise.race 包装在 try-finally 中,就像方法二一样。)

与方法二相比,唯一的小缺点是 sleep(5000) 将保留 运行,并且不会在 someTask 完成时立即取消(即使 result 立即可用),这可能会阻止您的程序立即退出。