Promise resolve() 被调用,但直到它被调用的循环终止才解决

Posted

技术标签:

【中文标题】Promise resolve() 被调用,但直到它被调用的循环终止才解决【英文标题】:Promise resolve() called but does not resolve until loop it was called from terminates 【发布时间】:2022-01-23 06:42:33 【问题描述】:

我正在基于游戏循环在 nodejs 中编写应用程序。循环的每次迭代,它都会触发一个事件发射器,并像这样调用一个更新方法:

  updateLoop() 
    while (!this.windowShouldClose()) 
      this.onUpdate.emit();
      this.update();
    
  

似乎事件发射器可以帮助在游戏对象上编写异步函数,这些函数在不同操作之间等待帧,就像协程一样。我写了一个实用函数,它可以回调,或者在事件发射器的下一次发射时解决一个承诺:

nextFrame() 
  return new Promise((resolve) => 
    this.onUpdate.once(resolve); // event emitter will resolve promise on next emit()
  )


nextFrameCallback(callback) 
  this.onUpdate.once(callback);


// example use
async doThingsAsync() 
  // do something
  await this.nextFrame();
  // do something else on the next frame / loop iteration

虽然基于回调的函数按预期工作,但 nextFrame() 的承诺版本并没有在我预期的时间解决承诺。在我的示例中,await nextFrame() 仅在 updateLoop() 的外循环退出后才解析。我在 promise 上附加了一个 console.log 以了解更多信息,并发现控制台日志和 resolve() 确实在循环中被调用,但等待它仍然会等待循环完全终止。

nextFrameDebug() 
  return new Promise((resolve) => 
    this.onUpdate.once(() => 
      console.log('debug nextFrame'); // this prints during the expected loop iteration
      resolve(); // this does not actually resolve until updateLoop() terminates
    )
  )

这是一个演示上述功能的 JSFiddle:https://jsfiddle.net/8L4wub29/5/ 似乎我接近一个功能性解决方案,但我对承诺或异步函数有一些误解。这是否与从循环中调用异步函数有关?我如何编写 nextFrame() 以便承诺在循环的下一次迭代时解决,而不是在循环退出后解决?我意识到对于游戏中的大多数功能,毫秒超时更有用,但在某些情况下,例如等待物理更新,游戏可能只想等待一帧作为实用函数。基于回调的版本运行良好,但如果需要多次使用则需要嵌套,看起来不像使用await那么干净

【问题讨论】:

您是否有理由不将本机 async 函数用于您的 onUpdate 函数?您应该只直接使用Promise 来调整早于Promise 的旧API(例如window.setTimeout)或用于存根/测试。 Promise 不应该用于表示可重复的事件。为此,请考虑改用RxJS(RxJS 与 ReactJS 完全无关,bte)。 我正在调用 node-addon-api C 库来在这个应用程序中进行图形/渲染,它的 API 将在自动完成绘图后等待一帧的剩余持续时间,我假设都是同步发生的。 "并且它的 API 会在自动绘制完成后等待一帧的剩余持续时间" - 呃,这听起来可怕(在 JS 中,什么都没有 i> 应该“等待”或阻塞线程)。您可能在滥用该库。请告诉我们更多关于你在做什么。 我仍在编写绑定,所以文档有点稀疏。 github.com/twuky/raylib-4.0 是它的仓库。据我所知,无论目标 FPS 发送到什么位置,BeginDrawing() 和 EndDrawing() 调用之间似乎总是占用一帧的持续时间。在我看来,您是在暗示主更新循环本身是一个异步函数,如果该循环只是更新游戏逻辑,那么绘制调用可能会在其他地方同步发生? 【参考方案1】:

Promises 保证它们的.then 回调将总是被异步调用。这意味着如果你有一些随机的承诺,并且你做这样的事情:

somePromise.then(() => 
  console.log('inside');
);
console.log('outside');

即使promise 恰好已经处于已解决状态,您也会始终看到“outside”后跟“inside”。 Promise 是这样设计的,因此您可以确定执行顺序,因此不必修复涉及两个顺序的痛苦错误。当您使用 await 而不是显式的 .then 时也是如此:await 之后的代码必须等到 promise 解决,并且调用堆栈返回到系统代码。

因此,当您从回调切换到 Promise 时,您从可以在循环中间同步调用回调的代码转到必须等待执行返回的代码。只有一次updateLoop 返回或产生,promise 之后的代码才能运行。由于该循环在结束之前永远不会返回或产生,因此所有承诺都会延迟到循环之后。

如果您需要 updateLoop 同步运行,那么您不能为此使用 Promise。如果相反 updateLoop 应该是异步的(也许在运行下一步之前设置超时),那么可以使用 Promise,但我需要更多关于你试图做什么的细节来举个例子。

【讨论】:

这个解释对我来说很有意义。听起来,在我的特定用例中,渲染步骤正在同步等待,任何需要多个“帧”的类例程行为都需要基于回调才能运行。我使用 promise 方法的主要目标只是将 await 语法用于更简洁的函数【参考方案2】:

回答我自己的问题,因为我找到了提供预期功能的东西。我只是将示例中的 updateLoop() 调用更新为此。

  async updateLoop() 
    while (!this.windowShouldClose()) 
      await null;
      this.onUpdate.emit();
      this.update();
    
  

是否有人能够更清楚地评论为什么现在会产生预期的效果?除非我添加 await 语句,否则将函数更改为 async 不会影响 promise 解决的点。我的假设包括等待语句释放事件循环,最终允许承诺解决。 如果您使用此函数编辑上述 JSfiddle,则控制台中打印的数字将与打印语句“预期”的内容不一致 - 这仅与计数器增加有关。执行顺序似乎是正确的。

【讨论】:

当代码执行遇到await 时,该函数会返回一个promise(如果这是第一个等待),否则会产生。无论哪种方式,代码执行都会停止,从而使其他代码有机会运行。你的这个想法确实有效。 /thumbsup

以上是关于Promise resolve() 被调用,但直到它被调用的循环终止才解决的主要内容,如果未能解决你的问题,请参考以下文章

promise链式调用

Vue- Promise函数---参数resolve(调用.then方法)-- 参数reject(调用.catch方法)---链式结构

停止 angular-ui-router 导航,直到 promise 解决

Promise.then方法的返回值问题

如果一个promise永不resolve,会内存泄漏吗

ES6 promise