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

时间:2018-11-04 23:37:38

标签: javascript node.js constructor promise async-await

我正在玩Promise Extensions for JavaScript (prex),并且希望使用Promise classprex.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中此行为和其他一些异步/等待实现细节的好读物。

1 个答案:

答案 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的调用。

支持证据

  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被欺骗返回其操作数):

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 specification

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

它(await)还对操作数承诺调用then以实现或拒绝中间承诺。这个特定的then调用返回类operandPromise.constructor的承诺。尽管从未使用过then返回的Promise,但在扩展类构造函数中进行登录会显示该调用。

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


附录:解密await specification

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

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

  4.   

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

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

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

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

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

  4.   
  5. 设置onFulfilled。[[AsyncContext]]为asyncContext。

  6.   

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

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

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

  4.   
  5. 设置onRejected。[[AsyncContext]]到asyncContext。

  6.   

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

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

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

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

  
      
  1. 从执行上下文堆栈中删除asyncContext,并将位于执行上下文堆栈顶部的执行上下文还原为正在运行的执行上下文。

  2.   
  3. 设置asyncContext的代码评估状态,以便在完成完成后恢复评估时,将执行调用Await的算法的以下步骤,并提供完成功能。

  4.   

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