如何同步访问 Promise 的结果?

How to access the result of a Promise synchronously?

让我先从我喜欢异步代码这一事实说起。我永远不会在生产中将异步代码包装在同步包装器中,但这仍然是我想学习如何做的事情。我说的是 Node.JS,而不是浏览器。有很多方法可以同步访问异步函数的结果,例如使用 child_process.spawnSync 或 worker 和 Atomics。这些方法的问题是:

let prom = Promise.resolve(4);
// It is now impossible (as far as I know) to access the result of prom synchronously

无法在 postMessage 调用中发送承诺,因此工作人员无法访问它们并等待它们同步完成或根本无法完成。有人可能会想,为什么不这样做:

let prom = Promise.resolve(4);
prom.then(res => global.result = res);
while (!global.result) {}; // Do nothing
// Once the loop finishes, the result *would* be available
console.log(global.result); // => 4

当然,这不行。事件循环甚至在开始处理prom.then的回调函数之前就等待执行完while循环。这会导致无限循环。所以这让我问,“是否有同步任务 必须 通常不执行以允许等待承诺?”

编辑
顺便说一句,我完全理解async/await。如果我使用的是 Node.JS 模块,那么我可以这样做:

let resolved = await prom;

或者如果我不是,我可以将整个脚本包装在一个 async 函数中并执行相同的操作。但是,目标是能够访问结果,避免等待或在异步上下文中使用 await,从而能够从同步上下文访问和等待。

我找到了 npm 包 deasync。其中,有一行代码:process._tickCallback()。我将它实现为一个函数。这将 解决已完全实现 的承诺。例如:

function halt(promise) {
    let result;
    promise.then(res=>result=res);
    while(!result) process._tickCallback();
    return result;
};

let ans = halt(Promise.resolve(123));
console.log(ans); //=> 123

该功能可能会随时消失,因为它没有包含在文档中。如果 promise 未决,则 while 循环将是无限的。

这应该是不可能的。 ECMAScript 规范禁止它。

首先请注意,ECMAScript 规范在提到与承诺相关的异步代码执行时会提到“作业”。例如,在 Promise Objects:

A promise p is fulfilled if p.then(f, r) will immediately enqueue a Job to call the function f.

作业需要仅在调用堆栈为空时执行。例如,它指定 Jobs and Host Operations to Enqueue Jobs:

A Job is an abstract closure with no parameters that initiates an ECMAScript computation when no other ECMAScript computation is currently in progress.

Their implementations must conform to the following requirements:

  • At some future point in time, when there is no running execution context and the execution context stack is empty, the implementation must:
    • [...] Call the abstract closure

您指出了 Node 的未记录和 deprecated process._tickCallback() 方法,这显然违反了规范,因为它允许 promise 作业队列中的作业在有非清空调用堆栈。

因此,使用此类构造不是好的做法,因为其他代码不能再依赖上述异步作业执行原则。

具体到 process._tickCallback():如果它用于允许承诺调用其 then 回调,则当该承诺依赖于其他一些异步 API,例如setTimeout。例如,以下代码片段将永远循环,因为超时作业永远不会执行:

let result = 0;
new Promise(resolve => 
    setTimeout(resolve, 10)
).then(() => result = 1);

while(!result) process._tickCallback();

console.log("all done")

异步

在JavaScript异步代码运行s(必须运行)时调用栈为空。不应该试图让它以不同的方式工作。编码人员应该完全接受这种模式。