繁体   English   中英

自定义 Promise 类的构造函数被调用两次(扩展标准 Promise)

[英]Constructor of a custom promise class is called twice (extending standard Promise)

我正在使用Promise Extensions for JavaScript (prex)并且我想使用prex.CancellationToken扩展具有取消支持的标准Promise 类完整代码请点击此处

出乎意料的是,我看到我的自定义类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.

为什么有两次 CancelablePromise CancellablePromise::constructor调用?

我尝试使用 VSCode 设置断点。 第二次命中的堆栈跟踪显示它是从runMicrotasks ,它本身是从_tickCallback某处的_tickCallback调用的。

更新后,谷歌现在有“引擎盖下的等待”博客文章,这是理解这种行为和 V8 中其他一些异步/等待实现细节的好读物。

更新,因为我不断回到这个,添加static get [Symbol.species]() { return Promise; } static get [Symbol.species]() { return Promise; }CancellablePromise类就解决了这个问题

第一次更新:

我首先想到 'main' 之后的.catch( callback)会返回一个新的、未决的扩展 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.js 中被调用两次。

现在await在其操作数上调用Promise.resolve (编辑:或者它可能在早期 JS 引擎的 async/await 版本中没有严格按照标准实现)

如果操作数是一个 promise,其构造函数是 Promise,则Promise.resolve返回操作数不变。

如果操作数是一个 thenable ,其构造函数不是PromisePromise.resolve使用 onfulfilled 和 onRejected 处理程序调用操作数的 then 方法,以便收到操作数已解决状态的通知。 then调用创建和返回的承诺属于扩展类,并解释了对 CancellablePromise.prototype.constructor 的第二次调用。

支持证据

  1. new CancellablePromise().constructorCancellablePromise

 class CancellablePromise extends Promise { constructor(executor) { super(executor); } } console.log ( new CancellablePromise( ()=>null).constructor.name);

  1. 更改CancellablePromise.prototype.constructorPromise用于测试目的只引起一个呼叫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();


第二次更新(非常感谢 OP 提供的链接)

符合要求的实现

根据await规范

await创建一个匿名的中间Promise承诺,带有 onFulilled 和 onRejected 处理程序,以在await运算符之后恢复执行或从中抛出错误,具体取决于中间承诺实现的稳定状态。

它( await )还呼吁then在操作承诺履行或拒绝中间承诺。 这个特殊的then调用返回一个类操作数operandPromise.constructor 尽管从不使用then返回的承诺,但在扩展类构造函数中记录会显示调用。

如果出于实验目的将扩展承诺的constructor值改回Promise ,则上述then调用将默默地返回一个 Promise 类承诺。


附录:解密await规范

  1. 让 asyncContext 成为正在运行的执行上下文。

  2. 让 promiseCapability 成为可能! NewPromiseCapability(%Promise%)。

使用promiseresolvereject属性创建一个新的类似 jQuery 的延迟对象,而是将其称为“PromiseCapability Record”。 deferred 的promise对象属于(全局)基础Promise构造函数类。

  1. 履行 ! 调用(promiseCapability.[[Resolve]], undefined, « promise »)。

使用await的正确操作数解析延迟的承诺。 如果操作数是“thenable”,解析过程要么调用该操作数的then方法,要么在操作数是其他非承诺值时实现延迟承诺。

  1. 让 stepsFulfilled 成为 Await Fulfilled Functions 中定义的算法步骤。

  2. 让 onFulfilled 成为 CreateBuiltinFunction(stepsFulfilled, « [[AsyncContext]] »)。

  3. 将 onFulfilled.[[AsyncContext]] 设置为 asyncContext。

通过返回作为参数传递给处理程序的操作数的已完成值,在调用它的async函数内创建一个 onfulfilled 处理程序以恢复await操作。

  1. 让stepsRejected 成为Await Rejected Functions 中定义的算法步骤。

  2. 让 onRejected 成为 CreateBuiltinFunction(stepsRejected, « [[AsyncContext]] »)。

  3. 将 onRejected.[[AsyncContext]] 设置为 asyncContext。

创建一个 onrejected 处理程序来恢复await操作,在它被调用的async函数内,通过抛出作为参数传递给处理程序的承诺拒绝原因。

  1. 履行 ! PerformPromiseThen(promiseCapability.[[Promise]], onFulfilled, onRejected).

使用这两个处理程序调用then延迟承诺,以便await可以响应其正在解决的操作数。

这个使用三个参数的调用是一种优化,有效地意味着then已在内部调用,并且不会从调用中创建或返回承诺。 因此,延迟的结算将调用其结算处理程序之一到承诺作业队列以供执行,但没有额外的副作用。

  1. 从执行上下文堆栈中移除 asyncContext 并将执行上下文堆栈顶部的执行上下文恢复为运行执行上下文。

  2. 设置 asyncContext 的代码评估状态,这样当评估通过 Completion 完成恢复时,将执行调用 Await 的算法的以下步骤,完成可用。

存储成功await后恢复的位置并返回到事件循环或微任务队列管理器。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM