我正在玩Promise Extensions for JavaScript (prex),并且希望使用Promise class,prex.CancellationToken通过取消支持扩展标准complete code here。
出乎意料的是,我看到自定义类CancellablePromise
的构造函数被调用了两次。为简化起见,我现在简化了所有取消逻辑,只保留了重现该问题所需的最低限度:
class CancellablePromise extends Promise {
constructor(executor) {
console.log("CancellablePromise::constructor");
super(executor);
}
}
function delayWithCancellation(timeoutMs, token) {
// TODO: we've stripped all cancellation logic for now
console.log("delayWithCancellation");
return new CancellablePromise(resolve => {
setTimeout(resolve, timeoutMs);
}, token);
}
async function main() {
await delayWithCancellation(2000, null);
console.log("successfully delayed.");
}
main().catch(e => console.log(e));
使用node simple-test.js
运行它,我得到了:
delayWithCancellation CancellablePromise::constructor CancellablePromise::constructor successfully delayed.
为什么CancellablePromise::constructor
有两个调用?
我尝试使用VSCode设置断点。第二个匹配的堆栈跟踪显示它是从runMicrotasks
调用的,它本身是从Node内部某个地方的_tickCallback
调用的。
已更新,Google现在发布了"await under the hood"博客,这是了解V8中此行为和其他一些异步/等待实现细节的好读物。
答案 0 :(得分:2)
第一次更新:
我首先想到{main}之后的.catch( callback)
将返回扩展的Promise类的新的,未决的Promise,但这是不正确的-调用异步函数会返回Promise
Promise。
进一步削减代码,只产生未完成的承诺:
class CancellablePromise extends Promise {
constructor(executor) {
console.log("CancellablePromise::constructor");
super(executor);
}
}
async function test() {
await new CancellablePromise( ()=>null);
}
test();
显示在Firefox,Chrome和Node中两次调用了扩展构造函数。
现在await
在其操作数上调用Promise.resolve
。 (编辑:或者它可能是在未严格按照标准实现的早期JS引擎版本的async / await中执行的)
如果操作数是其构造函数为Promise的Promise,则Promise.resolve
将不变地返回操作数。
如果操作数是一个其构造函数不是Promise
的thenable,则Promise.resolve
会同时使用onfulfilled和onRejected处理程序调用该操作数的then方法,以便通知该操作数的稳定状态。对此调用then
的创建和返回的承诺属于扩展类,并且占第二次对CancellablePromise.prototype.constructor的调用。
new CancellablePromise().constructor
是CancellablePromise
class CancellablePromise extends Promise {
constructor(executor) {
super(executor);
}
}
console.log ( new CancellablePromise( ()=>null).constructor.name);
CancellablePromise.prototype.constructor
更改为Promise
只会导致对CancellablePromise
的一个调用(因为await
被欺骗返回其操作数):
class CancellablePromise extends Promise {
constructor(executor) {
console.log("CancellablePromise::constructor");
super(executor);
}
}
CancellablePromise.prototype.constructor = Promise; // TESTING ONLY
async function test() {
await new CancellablePromise( ()=>null);
}
test();
符合规范的实施
await
使用onFulilled和onRejected处理程序创建匿名的中间 Promise 承诺
在await
运算符之后恢复执行或抛出错误,具体取决于中间承诺实现的结算状态。
它(await
)还对操作数承诺调用then
以实现或拒绝中间承诺。这个特定的then
调用返回类operandPromise.constructor
的承诺。尽管从未使用过then
返回的Promise,但在扩展类构造函数中进行登录会显示该调用。
如果出于实验目的将扩展承诺的constructor
值改回Promise
,则上述then
调用将静默返回Promise类承诺
附录:解密await
specification
让asyncContext作为正在运行的执行上下文。
让promiseCapability成为! NewPromiseCapability(%Promise%)。
使用promise
,resolve
和reject
属性创建一个新的类似jQuery的递延对象,将其称为“ PromiseCapability Record”。延迟的promise
对象属于(全局)基 Promise 构造函数类。
- 执行!呼叫(promiseCapability。[[解决]],未定义,«承诺»)。
使用正确的await
操作数来解析延迟的承诺。解析过程或者调用操作数的then
方法(如果它是“ thenable”),或者如果操作数是其他非承诺值,则执行递延的诺言。
让stepsFulfilled为“等待实现的功能”中定义的算法步骤。
让CreateBuiltinFunction(stepsFulfilled,«[[AsyncContext]]»)可以实现。
设置onFulfilled。[[AsyncContext]]为asyncContext。
通过返回作为参数传递给处理程序的操作数的实现值,在调用的await
函数内部创建一个未完成的处理程序以恢复async
操作。
让步骤拒绝为“等待拒绝功能”中定义的算法步骤。
让onRejected成为CreateBuiltinFunction(stepsRejected,«[[AsyncContext]]»)。
设置onRejected。[[AsyncContext]]到asyncContext。
通过抛出传递给处理程序作为其参数的承诺拒绝原因,在调用的await
函数内部创建一个onrejected处理程序以恢复async
操作。
- 执行! PerformPromiseThen(promiseCapability。[[Promise]],onFulfilled,onRejected)。
使用这两个处理程序在延迟的Prom上调用then
,以便await
可以响应其操作数被结算。
此调用使用三个参数是一种优化,有效地意味着then
已在内部被调用,并且不会在调用中创建或返回承诺。因此,延迟的结算将把调用它的结算处理程序之一调度到promise作业队列中执行,但是没有其他副作用。
从执行上下文堆栈中删除asyncContext,并将位于执行上下文堆栈顶部的执行上下文还原为正在运行的执行上下文。
设置asyncContext的代码评估状态,以便在完成完成后恢复评估时,将执行调用Await的算法的以下步骤,并提供完成功能。
存储成功await
之后要恢复的位置,并返回到事件循环或微任务队列管理器。