自定义Promise类的构造函数被调用两次(扩展了标准Promise) [英] Constructor of a custom promise class is called twice (extending standard Promise)

查看:72
本文介绍了自定义Promise类的构造函数被调用两次(扩展了标准Promise)的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在玩 JavaScript的承诺扩展(prex),我想扩展标准的承诺类,并具有取消支持 prex.CancellationToken

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.

出乎意料的是,我看到自定义类CancellablePromise的构造函数被调用了两次.为简化起见,我现在简化了所有取消逻辑,只保留了重现该问题所需的最低限度:

Unexpectedly, I'm seeing the constructor of my custom class CancellablePromise being called twice. 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));

使用node simple-test.js运行它,我得到了:

Running it with node simple-test.js, I'm getting this:


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

为什么有两个CancellablePromise::constructor调用?

Why are there two invocations of CancellablePromise::constructor?

我尝试使用VSCode设置断点.第二个命中的堆栈跟踪显示它是从runMicrotasks调用的,它本身是从Node内部某个地方的_tickCallback调用的.

I tried setting breakpoints with VSCode. The stack trace for the second hit shows it's called from runMicrotasks, which itself is called from _tickCallback somewhere inside Node.

已更新,Google现在具有"博客文章,这是一本很好的书,有助于您了解这种行为以及V8中其他一些异步/等待实现的细节.

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.

推荐答案

首次更新:

我首先想到在'main'之后的.catch( callback)将返回扩展的Promise类的新的,未决的Promise,但这是不正确的-调用异步函数将返回Promise Promise.

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.

进一步削减代码,仅产生未完成的承诺:

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

显示在Firefox,Chrome和Node中两次调用了扩展构造函数.

shows the extended constructor being called twice in Firefox, Chrome and Node.

现在await在其操作数上调用Promise.resolve. (或者它可能是在未严格按照标准实施的早期JS引擎版本的async/await中执行的)

Now await calls Promise.resolve on its operand. ( or it probably did in early JS engine's versions of async/await not strictly implemented to standard)

如果操作数是构造函数为Promise的Promise,则Promise.resolve不变地返回操作数.

If the operand is a promise whose constructor is Promise, Promise.resolve returns the operand unchanged.

如果操作数是一个其构造函数不是Promise的thenable,则Promise.resolve会同时使用onfulfilled和onRejected处理程序调用该操作数的then方法,以便通知该操作数的稳定状态.对此then的调用所创建和返回的promise是扩展类,并且占第二次对CancellablePromise.prototype.constructor的调用.

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. 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.

  1. new CancellablePromise().constructorCancellablePromise

class CancellablePromise extends Promise {
  constructor(executor) {
    super(executor);
  }
}

console.log ( new CancellablePromise( ()=>null).constructor.name);

  1. 出于测试目的将CancellablePromise.prototype.constructor更改为Promise只会导致对CancellablePromise的一次调用(因为await被骗回了其操作数):
  1. Changing CancellablePromise.prototype.constructor to Promise for testing purposes causes only one call to CancellablePromise (because await is fooled into returning its operand) :

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提供的链接)


Second Update (with huge thanks to links provided by the OP)

符合规范的实施

根据 await规范

await使用onFulilled和onRejected处理程序创建匿名的中间 Promise 承诺 在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)还会在操作数承诺上调用then以实现或拒绝中间承诺.此特定的then调用返回类operandPromise.constructor的承诺.尽管从不使用then返回的Promise,但在扩展类构造函数中进行日志记录会显示该调用.

It (await) also calls then on the operand promise to fulfill or reject the intermediate promise. This particular then call returns a promise of class operandPromise.constructor. Although the then returned promise is never used, logging within an extended class constructor reveals the call.

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

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.

附录:解密 await规范

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

  1. Let asyncContext be the running execution context.

让promiseCapability成为! NewPromiseCapability(%Promise%).

Let promiseCapability be ! NewPromiseCapability(%Promise%).

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

Creates a new jQuery-like deferred object with promise, resolve and reject properties, calling it a "PromiseCapability Record" instead. The deferred's promise object is of the (global) base Promise constructor class.

  1. 执行!呼叫(promiseCapability.[[解决]],未定义,«承诺»).

使用正确的操作数await解析延迟的承诺.解析过程或者调用操作数的then方法(如果它是"thenable"),或者如果操作数是其他非承诺值,则执行递延的诺言.

Resolve the deferred promise with the right operand of 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.

  1. 让stepsFulfilled为等待实现的功能"中定义的算法步骤.

  1. Let stepsFulfilled be the algorithm steps defined in Await Fulfilled Functions.

让CreateBuiltinFunction(stepsFulfilled,«[[AsyncContext]]»»)可以实现.

Let onFulfilled be CreateBuiltinFunction(stepsFulfilled, « [[AsyncContext]] »).

设置onFulfilled.[[AsyncContext]]为asyncContext.

Set onFulfilled.[[AsyncContext]] to asyncContext.

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

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.

  1. 让步骤拒绝为等待拒绝功能"中定义的算法步骤.

  1. Let stepsRejected be the algorithm steps defined in Await Rejected Functions.

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

Let onRejected be CreateBuiltinFunction(stepsRejected, « [[AsyncContext]] »).

设置onRejected.[[AsyncContext]]到asyncContext.

Set onRejected.[[AsyncContext]] to asyncContext.

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

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.

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

使用这两个处理程序在延迟的Prom上调用then,以便await可以响应其操作数被结算.

Call then on the deferred promise with these two handlers so that await can respond to its operand being settled.

此调用使用三个参数是一种优化,可以有效地表示then已在内部被调用,并且不会在调用中创建或返回承诺.因此,延迟的结算将把调用它的结算处理程序之一调度到Promise作业队列中执行,但是没有其他副作用.

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. 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. 从执行上下文堆栈中删除asyncContext,并将位于执行上下文堆栈顶部的执行上下文还原为正在运行的执行上下文.

  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的代码评估状态,以便在完成完成后恢复评估时,将执行调用Await的算法的以下步骤,并提供完成功能.

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.

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

Store where to resume after a successful await and return to the event loop or micro task queue manager.

这篇关于自定义Promise类的构造函数被调用两次(扩展了标准Promise)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆