promise 中的超时循环在 promise 解决后永远不会执行?

timeout loop in promise never executes after promise is resolved?

我 运行 遇到一个问题,即从已解决的承诺发送到 setTimeout 的回调永远不会执行。

假设我有以下内容:

class Foo {
  constructor(foo) {
    this.foo = foo;
  }

  async execUntilStop(callback) {
    const timeoutLoopCallback = () => {
      if (this.stopExec) return;
      callback({ data: 'data' });
      setTimeout(timeoutLoopCallback, 10);
    };
    setTimeout(timeoutLoopCallback, 10);

    return { data: 'data'};
  }

  stop() {
    this.stopExec = true;
  }
}

const myFunc = async function () {
  let callbackCalled = false;
  const callback = () => callbackCalled = true;
  foo = new Foo('foo');
  foo.execUntilStop(callback);
  const hasCallbackCalled = async () => callbackCalled;

  while(!(await hasCallbackCalled())) null;
  foo.stop();
  return 'success!';
};

myFunc().then((result) => console.log(result))

myFunc() 永远不会解析,因为它一直在等待 callbackCalled 成为 true

我在这里错过了什么?我相信事件循环不应该被阻止,因为我在异步函数上调用 await 来检查回调是否被调用。我假设它与 timeoutLoopCallback 绑定到已解决的承诺有关,但我不是 javascript 专家,可以使用一些反馈。

注意:这看起来有点奇怪,但本质上这是 class 我正在尝试为其编写测试用例,它将不断执行回调直到停止。


已解决

利用我从@traktor53 的回答中学到的知识,我编写了一个方便的花花公子 wait 函数:

// resolves when callback returns true
const wait = callback => new Promise((resolve, reject) => {
  const end = () => {
    try {
      if (callback()) {
        resolve(true);
      } else {
        setTimeout(end, 0);
      }
    } catch(error) {
      reject(error);
    }
  };
  setTimeout(end, 0);
});


class Foo {
  constructor(foo) {
    this.foo = foo;
  }

  async execUntilStop(callback) {
    const timeoutLoopCallback = () => {
      if (this.stopExec) return;
      callback({ data: 'data' });
      setTimeout(timeoutLoopCallback, 10);
    };
    setTimeout(timeoutLoopCallback, 10);

    return { data: 'data'};
  }

  stop() {
    this.stopExec = true;
  }
}

const myFunc = async function (num) {
  let callbackCalled = false;
  const callback = () => callbackCalled = true;
  foo = new Foo('foo');
  foo.execUntilStop(callback);

  const hasCallbackCalled = () => callbackCalled;
  await wait(hasCallbackCalled);
  foo.stop();
  return 'success!';
};

myFunc().then((result) => console.log(result)); // => success!

处理 promise 结算的作业进入 ECMAScript 标准中描述的 "Promise Job Queue" (PJQ)。 HTML 文档中不常使用此术语。

浏览器(和至少一个脚本引擎)将 PJQ 的作业放入通常称为 "Micro Task Queue" (MTQ) 的内容中。事件循环任务管理器从事件循环的脚本调用中检查 return 上的 MTQ,看它是否有任何作业,如果有,将弹出并执行队列中最旧的作业。原来的那一行post

 while(!(await callbackCalled)) null;

(在第一次调用中相当于

while( !( await Promise.resolve( false));  // callbackCalled is false

)

将作业获取由 Promise.resolve 编辑的承诺 return 的结算值,放入 MTQ 并通过让 await 操作员 return 继续执行完成值,即 false.

因为浏览器处理 MTQ 的优先级高于计时器到期生成的任务,所以在 await 操作之后继续执行并立即执行循环的另一次迭代并将另一个作业放入 MTQ 以等待 false 值,不处理中间的任何定时器回调

这设置了一个异步无限循环(顺便说一句,恭喜,我以前从未见过!),在这种情况下,我不希望计时器回调执行并再次调用 timeoutLoopCallback .

无限循环也阻止继续到下一行:

  foo.stop()

从不执行。

请注意,此处观察到的阻塞效应是 HTML 实现 "Promise Job Queue" 的结果 - ECMAScript 承诺选择不指定实际 JavaScript 系统的实现和优先级细节。所以怪 HTML 标准,而不是 ECMAScript :D

另请注意:将 await calledBackCalled 替换为 await hasCallbackCalled() 不会解决问题 - 将生成不同的承诺作业,但 await 运算符仍将 return false


(更新)既然你问了,

的实际步骤
 while(!(await hasCallbackCalled())) null;

是:

  1. 评价hasCallbackCalled()
  2. 'hasCallbackCalled` 是一个异步函数,return 是一个由函数主体的 return 值实现的承诺。
  3. 函数体是同步代码,通过同步 returning callbackCalled 的值(即 false)在第一次调用时实现 returned promise
  4. 异步函数 return 的 promise 到目前为止已同步实现值 false
  5. await 现在通过在步骤 4 中获得的承诺上调用 .then 添加处理程序,让 await 知道结算值和状态(在本例中 "fulfilled" ).
  6. 但是调用 then 实现的承诺 同步 插入一个作业来调用具有实现值的 fulfilled 处理程序到 MTQ
  7. MTQ 现在可以调用此特定 await 的代码;
  8. await returns 到事件循环管理器。
  9. MTQ 作业现在执行第 5 步中添加的 then 处理程序,
  10. then 处理程序恢复 await 运算符处理,将 return 值 false 发送给用户脚本。
  11. while 循环测试从第 1 步继续执行。