简体   繁体   English

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

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

I'm playing with Promise Extensions for JavaScript (prex) and I want to extend the standard Promise class with cancellation support using prex.CancellationToken , complete code here .我正在使用Promise Extensions for JavaScript (prex)并且我想使用prex.CancellationToken扩展具有取消支持的标准Promise 类完整代码请点击此处

Unexpectedly, I'm seeing the constructor of my custom class CancellablePromise being called twice.出乎意料的是,我看到我的自定义类CancellablePromise的构造函数被调用了两次。 To simplify things, I've now stripped down all the cancellation logic and left just a bare minimum required to repro the issue:为了简化事情,我现在已经剥离了所有取消逻辑,只留下重现问题所需的最低限度:

 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));

Running it with node simple-test.js , I'm getting this:使用node simple-test.js运行它,我得到了这个:

delayWithCancellation
CancellablePromise::constructor
CancellablePromise::constructor
successfully delayed.

Why are there two invocations of CancellablePromise::constructor ?为什么有两次 CancelablePromise CancellablePromise::constructor调用?

I tried setting breakpoints with VSCode.我尝试使用 VSCode 设置断点。 The stack trace for the second hit shows it's called from runMicrotasks , which itself is called from _tickCallback somewhere inside Node.第二次命中的堆栈跟踪显示它是从runMicrotasks ,它本身是从_tickCallback某处的_tickCallback调用的。

Updated , Google now have "await under the hood" blog post which is a good read to understand this behavior and some other async/await implementation specifics in V8.更新后,谷歌现在有“引擎盖下的等待”博客文章,这是理解这种行为和 V8 中其他一些异步/等待实现细节的好读物。

Updated , as I keep coming back to this, adding static get [Symbol.species]() { return Promise; }更新,因为我不断回到这个,添加static get [Symbol.species]() { return Promise; } static get [Symbol.species]() { return Promise; } to the CancellablePromise class solves the problem . static get [Symbol.species]() { return Promise; }CancellablePromise类就解决了这个问题

First Update:第一次更新:

I first thought .catch( callback) after 'main' would return a new, pending promise of the extended Promise class, but this is incorrect - calling an async function returns a Promise promise.我首先想到 'main' 之后的.catch( callback)会返回一个新的、未决的扩展 Promise 类的承诺,但这是不正确的 - 调用异步函数会返回一个Promise承诺。

Cutting the code down further, to only produce a pending promise:进一步削减代码,只产生一个挂起的承诺:

 class CancellablePromise extends Promise { constructor(executor) { console.log("CancellablePromise::constructor"); super(executor); } } async function test() { await new CancellablePromise( ()=>null); } test();

shows the extended constructor being called twice in Firefox, Chrome and Node.显示扩展构造函数在 Firefox、Chrome 和 Node.js 中被调用两次。

Now await calls Promise.resolve on its operand.现在await在其操作数上调用Promise.resolve (Edit: or it probably did in early JS engine's versions of async/await not strictly implemented to standard) (编辑:或者它可能在早期 JS 引擎的 async/await 版本中没有严格按照标准实现)

If the operand is a promise whose constructor is Promise, Promise.resolve returns the operand unchanged.如果操作数是一个 promise,其构造函数是 Promise,则Promise.resolve返回操作数不变。

If the operand is a thenable whose constructor is not Promise , Promise.resolve calls the operand's then method with both onfulfilled and onRejected handlers so as to be notified of the operand's settled state.如果操作数是一个 thenable ,其构造函数不是PromisePromise.resolve使用 onfulfilled 和 onRejected 处理程序调用操作数的 then 方法,以便收到操作数已解决状态的通知。 The promise created and returned by this call to then is of the extended class, and accounts for the second call to CancellablePromise.prototype.constructor.then调用创建和返回的承诺属于扩展类,并解释了对 CancellablePromise.prototype.constructor 的第二次调用。

Supporting evidence支持证据

  1. new CancellablePromise().constructor is CancellablePromise new CancellablePromise().constructorCancellablePromise

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

  1. Changing CancellablePromise.prototype.constructor to Promise for testing purposes causes only one call to CancellablePromise (because await is fooled into returning its operand) :更改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();


Second Update (with huge thanks to links provided by the OP) 第二次更新(非常感谢 OP 提供的链接)

Conforming Implementations符合要求的实现

Per the await specification根据await规范

await creates an anonymous, intermediate Promise promise with onFulilled and onRejected handlers to either resume execution after the await operator or throw an error from it, depending on which settled state the intermediate promise achieves. await创建一个匿名的中间Promise承诺,带有 onFulilled 和 onRejected 处理程序,以在await运算符之后恢复执行或从中抛出错误,具体取决于中间承诺实现的稳定状态。

It ( await ) also calls then on the operand promise to fulfill or reject the intermediate promise.它( await )还呼吁then在操作承诺履行或拒绝中间承诺。 This particular then call returns a promise of class operandPromise.constructor .这个特殊的then调用返回一个类操作数operandPromise.constructor Although the then returned promise is never used, logging within an extended class constructor reveals the call.尽管从不使用then返回的承诺,但在扩展类构造函数中记录会显示调用。

If the constructor value of an extended promise is changed back to Promise for experimental purposes, the above then call will silently return a Promise class promise.如果出于实验目的将扩展承诺的constructor值改回Promise ,则上述then调用将默默地返回一个 Promise 类承诺。


Appendix: Decyphering the await specification附录:解密await规范

  1. Let asyncContext be the running execution context.让 asyncContext 成为正在运行的执行上下文。

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

Creates a new jQuery-like deferred object with promise , resolve and reject properties, calling it a "PromiseCapability Record" instead.使用promiseresolvereject属性创建一个新的类似 jQuery 的延迟对象,而是将其称为“PromiseCapability Record”。 The deferred's promise object is of the (global) base Promise constructor class. deferred 的promise对象属于(全局)基础Promise构造函数类。

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

Resolve the deferred promise with the right operand of await .使用await的正确操作数解析延迟的承诺。 The resolution process either calls the then method of the operand if it is a "thenable", or fulfills the deferred promise if the operand is some other, non-promise, value.如果操作数是“thenable”,解析过程要么调用该操作数的then方法,要么在操作数是其他非承诺值时实现延迟承诺。

  1. Let stepsFulfilled be the algorithm steps defined in Await Fulfilled Functions.让 stepsFulfilled 成为 Await Fulfilled Functions 中定义的算法步骤。

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

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

Create an onfulfilled handler to resume the await operation, inside the async function it was called in, by returning the fulfilled value of the operand passed as argument to the handler.通过返回作为参数传递给处理程序的操作数的已完成值,在调用它的async函数内创建一个 onfulfilled 处理程序以恢复await操作。

  1. Let stepsRejected be the algorithm steps defined in Await Rejected Functions.让stepsRejected 成为Await Rejected Functions 中定义的算法步骤。

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

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

Create an onrejected handler to resume the await operation, inside the async function it was called in, by throwing a promise rejection reason passed to the handler as its argument.创建一个 onrejected 处理程序来恢复await操作,在它被调用的async函数内,通过抛出作为参数传递给处理程序的承诺拒绝原因。

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

Call then on the deferred promise with these two handlers so that await can respond to its operand being settled.使用这两个处理程序调用then延迟承诺,以便await可以响应其正在解决的操作数。

This call using three parameters is an optimisation that effectively means then has been called internally and won't be creating or returning a promise from the call.这个使用三个参数的调用是一种优化,有效地意味着then已在内部调用,并且不会从调用中创建或返回承诺。 Hence settlement of the deferred will dispatch calling one of its settlement handlers to the promise job queue for execution, but has no additional side effects.因此,延迟的结算将调用其结算处理程序之一到承诺作业队列以供执行,但没有额外的副作用。

  1. Remove asyncContext from the execution context stack and restore the execution context that is at the top of the execution context stack as the running execution context.从执行上下文堆栈中移除 asyncContext 并将执行上下文堆栈顶部的执行上下文恢复为运行执行上下文。

  2. Set the code evaluation state of asyncContext such that when evaluation is resumed with a Completion completion, the following steps of the algorithm that invoked Await will be performed, with completion available.设置 asyncContext 的代码评估状态,这样当评估通过 Completion 完成恢复时,将执行调用 Await 的算法的以下步骤,完成可用。

Store where to resume after a successful await and return to the event loop or micro task queue manager.存储成功await后恢复的位置并返回到事件循环或微任务队列管理器。

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

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