异步/等待类构造函数

Posted

技术标签:

【中文标题】异步/等待类构造函数【英文标题】:Async/Await Class Constructor 【发布时间】:2017-09-11 21:28:33 【问题描述】:

目前,我正在尝试在类构造函数中使用async/await。这样我就可以为我正在进行的 Electron 项目获得一个自定义的 e-mail 标签。

customElements.define('e-mail', class extends htmlElement 
  async constructor() 
    super()

    let uid = this.getAttribute('data-uid')
    let message = await grabUID(uid)

    const shadowRoot = this.attachShadow(mode: 'open')
    shadowRoot.innerHTML = `
      <div id="email">A random email message has appeared. $message</div>
    `
  
)

目前,该项目无法运行,并出现以下错误:

Class constructor may not be an async method

有没有办法绕过这个,以便我可以在其中使用 async/await?而不是需要回调或 .then()?

【问题讨论】:

构造函数的目的是为你分配一个对象,然后立即返回。您能否更具体地说明究竟为什么您认为您的构造函数应该是异步的?因为我们几乎可以保证在这里处理XY problem。 @Mike'Pomax'Kamermans 这很有可能。基本上,我需要查询数据库以获取加载此元素所需的元数据。查询数据库是一个异步操作,因此在构造元素之前我需要某种方式来等待它完成。我宁愿不使用回调,因为我在整个项目的其余部分都使用了 await/async,并且希望保持连续性。 @Mike'Pomax'Kamermans 完整的上下文是一个电子邮件客户端,其中每个 HTML 元素看起来都类似于&lt;e-mail data-uid="1028"&gt;&lt;/email&gt;,并使用customElements.define() 方法填充信息。跨度> 你几乎不希望构造函数是异步的。创建一个返回对象的同步构造函数,然后使用.init() 之类的方法来执行异步操作。另外,由于您是子类 HTMLElement,因此使用此类的代码极有可能不知道它是异步的,因此您可能不得不寻找完全不同的解决方案。 相关:Is it bad practice to have a constructor function return a Promise? 【参考方案1】:

永远不会起作用。

async 关键字允许 await 在标记为 async 的函数中使用,但它也将该函数转换为 promise 生成器。所以标有async 的函数会返回一个promise。另一方面,构造函数返回它正在构造的对象。因此,我们遇到了一种情况,您希望同时返回一个对象和一个 Promise:这是一种不可能的情况。

您只能在可以使用 Promise 的地方使用 async/await,因为它们本质上是 Promise 的语法糖。不能在构造函数中使用 Promise,因为构造函数必须返回要构造的对象,而不是 Promise。

有两种设计模式可以克服这个问题,它们都是在 Promise 出现之前发明的。

    使用init() 函数。这有点像 jQuery 的.ready()。您创建的对象只能在它自己的initready 函数中使用:

    用法:

    var myObj = new myClass();
    myObj.init(function() 
        // inside here you can use myObj
    );
    

    实施:

    class myClass 
        constructor () 
    
        
    
        init (callback) 
            // do something async and call the callback:
            callback.bind(this)();
        
    
    

    使用构建器。我没有看到这在 javascript 中被大量使用,但是当需要异步构造对象时,这是 Java 中更常见的解决方法之一。当然,构建器模式在构造需要大量复杂参数的对象时使用。这正是异步构建器的用例。不同之处在于异步构建器不返回对象,而是返回该对象的承诺:

    用法:

    myClass.build().then(function(myObj) 
        // myObj is returned by the promise, 
        // not by the constructor
        // or builder
    );
    
    // with async/await:
    
    async function foo () 
        var myObj = await myClass.build();
    
    

    实施:

    class myClass 
        constructor (async_param) 
            if (typeof async_param === 'undefined') 
                throw new Error('Cannot be called directly');
            
        
    
        static build () 
            return doSomeAsyncStuff()
               .then(function(async_result)
                   return new myClass(async_result);
               );
        
    
    

    使用 async/await 实现:

    class myClass 
        constructor (async_param) 
            if (typeof async_param === 'undefined') 
                throw new Error('Cannot be called directly');
            
        
    
        static async build () 
            var async_result = await doSomeAsyncStuff();
            return new myClass(async_result);
        
    
    

注意:尽管在上面的示例中,我们为异步构建器使用了 Promise,但严格来说,它们并不是必需的。您可以轻松编写一个接受回调的构建器。


关于在静态函数中调用函数的注意事项。

这与异步构造函数无关,但与关键字 this 的实际含义有关(对于来自自动解析方法名称的语言的人来说,这可能有点令人惊讶,也就是说,不不需要this 关键字)。

this 关键字指的是实例化的对象。不是班级。因此,您通常不能在静态函数中使用this,因为静态函数未绑定到任何对象,而是直接绑定到类。

也就是说,在下面的代码中:

class A 
    static foo () 

你不能这样做:

var a = new A();
a.foo() // NOPE!!

您需要将其称为:

A.foo();

因此,以下代码会导致错误:

class A 
    static foo () 
        this.bar(); // you are calling this as static
                    // so bar is undefinned
    
    bar () 

要修复它,您可以将 bar 设为常规函数或静态方法:

function bar1 () 

class A 
    static foo () 
        bar1();   // this is OK
        A.bar2(); // this is OK
    

    static bar2 () 

【讨论】:

请注意,基于 cmets,想法是这是一个 html 元素,它通常没有手动 init() 但具有与某些特定属性相关的功能,例如 srchref(在本例中为data-uid),这意味着每次绑定一个新值时(也可能在构造过程中,但当然无需等待生成的代码)使用一个 setter 来绑定并启动 init路径) 您应该评论为什么以下答案不充分(如果是的话)。或以其他方式解决。 @AlexanderCraggs 只是为了方便,回调中的this 指的是myClass。如果你总是使用 myObj 而不是 this 你不需要它 目前是对语言的限制,但我不明白为什么将来你不能像我们拥有常规函数和异步函数一样拥有const a = await new A() 我不明白这是不可能的。异步函数最终仍会返回,只是被推迟了。异步函数可以像普通函数一样愉快地返回,你只需要等待它。没有根本的不匹配。正如你在下面看到的,已经有人解决了。【参考方案2】:

可以绝对做到这一点,方法是从构造函数返回Immediately Invoked Async Function Expression。 IIAFE 是在 top-level await 可用之前在异步函数之外使用 await 所需的非常常见模式的花哨名称:

(async () => 
  await someFunction();
)();

我们将使用这种模式在构造函数中立即执行异步函数,并将其结果返回为this

// Sample async function to be used in the async constructor
async function sleep(ms) 
  return new Promise(resolve => setTimeout(resolve, ms));



class AsyncConstructor 
  constructor(value) 
    return (async () => 

      // Call async functions here
      await sleep(500);
      
      this.value = value;

      // Constructors return `this` implicitly, but this is an IIFE, so
      // return `this` explicitly (else we'd return an empty object).
      return this;
    )();
  


(async () => 
  console.log('Constructing...');
  const obj = await new AsyncConstructor(123);
  console.log('Done:', obj);
)();

要实例化类,请使用:

const instance = await new AsyncConstructor(...);

对于 TypeScript,您需要 assert 构造函数的类型是类类型,而不是返回类类型的承诺:

class AsyncConstructor 
  constructor(value) 
    return (async (): Promise<AsyncConstructor> => 
      // ...
      return this;
    )() as unknown as AsyncConstructor;  // <-- type assertion
  

缺点

    使用异步构造函数扩展类会有限制。如果您需要在派生类的构造函数中调用super,则必须在没有await 的情况下调用它。如果您需要使用await 调用超级构造函数,您将遇到TypeScript 错误2337:Super calls are not permitted outside constructors or in nested functions inside constructors. 有人认为have a constructor function return a Promise 是一种“不好的做法”。

在使用此解决方案之前,确定您是否需要扩展类,并记录必须使用await 调用构造函数。

【讨论】:

@Downgoat:嗨,Vihan,我根据您的回答编写了一个测试用例,并在此线程中发布了它***.com/questions/43431550/… 如何在具有异步构造函数的子类中使用 super()?有什么解决方案吗?更具体地说:父类也有一个异步构造函数。 @user2912903 这是一个不足之处......无法找到一种惯用的方法来做到这一点,因为 super() 不幸的是没有返回父构造函数返回的内容。最好的解决方法是拥有一些异步超类调用的 init() 方法并在子类中覆盖它 打字稿问题:github.com/microsoft/TypeScript/issues/38519 如何从AsyncConstructor 只创建一个实例并使用export default await new AsyncConstructor(); 将其共享到其他文件?这不起作用,因为我们只能在 async 函数中使用 await。但在我的示例中没有功能。【参考方案3】:

因为异步函数是承诺,你可以在你的类上创建一个静态函数,它执行一个返回类实例的异步函数:

class Yql 
  constructor () 
    // Set up your class
  

  static init () 
    return (async function () 
      let yql = new Yql()
      // Do async stuff
      await yql.build()
      // Return instance
      return yql
    ())
    

  async build () 
    // Do stuff with await if needed
  


async function yql () 
  // Do this instead of "new Yql()"
  let yql = await Yql.init()
  // Do stuff with yql instance


yql()

从异步函数调用let yql = await Yql.init()

【讨论】:

这不就是slebetman's answer在2017年描述的build模式吗?【参考方案4】:

不像其他人说的那样,你可以让它工作。

JavaScript classes 可以从它们的constructor 返回任何东西,甚至是另一个类的实例。因此,您可能会从解析为实际实例的类的构造函数返回 Promise

下面是一个例子:

export class Foo 

    constructor() 

        return (async () => 

            // await anything you want

            return this; // Return the newly-created instance
        )();
    

然后,您将通过这种方式创建Foo 的实例:

const foo = await new Foo();

【讨论】:

call 的参数被忽略,因为它是一个箭头函数。 你是对的,@罗伯特。这都是我的错。我会在一段时间内更新我的答案——用普通的函数调用替换 .call(this) 调用应该没问题。感谢您指出这一点 好像和Downgoat比你早半年回答的一样。我没有看到转发相同想法的意义。 我很抱歉,@trincot,发布了同样的内容。写我的时候我没有注意到 Downgoat 的回答。我希望我的回答不会弄乱整个 *** 问题;)我尊重你的反对意见,尽管我不完全理解这对社区有什么帮助,因为我的行为不是故意的。 @DavideCannizzo:简单地删除这个答案怎么样?你会得到一个special badge。【参考方案5】:

权宜之计

您可以创建一个async init() ... return this; 方法,然后在您通常只说new MyClass() 时改为使用new MyClass().init()

这并不干净,因为它依赖于使用您的代码的每个人,以及您自己,总是像这样实例化对象。但是,如果您只在代码中的一两个特定位置使用此对象,则可能没问题。

由于 ES 没有类型系统,因此出现了一个重大问题,因此如果您忘记调用它,您只是返回了 undefined,因为构造函数没有返回任何内容。哎呀。更好的做法是:

最好的办法是:

class AsyncOnlyObject 
    constructor() 
    
    async init() 
        this.someField = await this.calculateStuff();
    

    async calculateStuff() 
        return 5;
    


async function newAsync_AsyncOnlyObject() 
    return await new AsyncOnlyObject().init();


newAsync_AsyncOnlyObject().then(console.log);
// output: AsyncOnlyObject someField: 5

工厂方法解决方案(略好)

但是你可能不小心做了新的 AsyncOnlyObject,你应该直接创建使用 Object.create(AsyncOnlyObject.prototype) 的工厂函数:

async function newAsync_AsyncOnlyObject() 
    return await Object.create(AsyncOnlyObject.prototype).init();


newAsync_AsyncOnlyObject().then(console.log);
// output: AsyncOnlyObject someField: 5

但是说你想在许多对象上使用这种模式......你可以将它抽象为一个装饰器或你(详细,呃)在定义像 postProcess_makeAsyncInit(AsyncOnlyObject) 之后调用的东西,但在这里我将使用 @987654330 @ 因为它有点适合子类语义(子类是父类+额外的,因为它们应该遵守父类的设计合同,并且可以做其他事情;如果父类也不是异步的,异步子类会很奇怪,因为它不能以相同的方式初始化):


抽象解决方案(扩展/子类版本)

class AsyncObject 
    constructor() 
        throw new Error('classes descended from AsyncObject must be initialized as (await) TheClassName.anew(), rather than new TheClassName()');
    

    static async anew(...args) 
        var R = Object.create(this.prototype);
        R.init(...args);
        return R;
    


class MyObject extends AsyncObject 
    async init(x, y=5) 
        this.x = x;
        this.y = y;
        // bonus: we need not return 'this'
    


MyObject.anew('x').then(console.log);
// output: MyObject x: "x", y: 5

(不要在生产中使用:我没有考虑过复杂的场景,例如这是否是为关键字参数编写包装器的正确方法。)

【讨论】:

【参考方案6】:

根据您的 cmets,您可能应该做所有其他带有资产加载的 HTMLElement 所做的事情:使构造函数启动一个侧加载操作,根据结果生成加载或错误事件。

是的,这意味着使用 Promise,但这也意味着“以与所有其他 HTML 元素相同的方式做事”,所以你是一个很好的伙伴。例如:

var img = new Image();
img.onload = function(evt)  ... 
img.addEventListener("load", evt => ... );
img.onerror = function(evt)  ... 
img.addEventListener("error", evt => ... );
img.src = "some url";

这将启动源资产的异步加载,当它成功时,以onload 结束,当它出错时,以onerror 结束。所以,让你自己的班级也这样做:

class EMailElement extends HTMLElement 
  connectedCallback() 
    this.uid = this.getAttribute('data-uid');
  

  setAttribute(name, value) 
    super.setAttribute(name, value);
    if (name === 'data-uid') 
      this.uid = value;
    
  

  set uid(input) 
    if (!input) return;
    const uid = parseInt(input);
    // don't fight the river, go with the flow, use a promise:
    new Promise((resolve, reject) => 
      yourDataBase.getByUID(uid, (err, result) => 
        if (err) return reject(err);
        resolve(result);
      );
    )
    .then(result => 
      this.renderLoaded(result.message);
    )
    .catch(error => 
      this.renderError(error);
    );
  
;

customElements.define('e-mail', EmailElement);

然后你让 renderLoaded/renderError 函数处理事件调用和影子 dom:

  renderLoaded(message) 
    const shadowRoot = this.attachShadow(mode: 'open');
    shadowRoot.innerHTML = `
      <div class="email">A random email message has appeared. $message</div>
    `;
    // is there an ancient event listener?
    if (this.onload) 
      this.onload(...);
    
    // there might be modern event listeners. dispatch an event.
    this.dispatchEvent(new Event('load'));
  

  renderFailed() 
    const shadowRoot = this.attachShadow(mode: 'open');
    shadowRoot.innerHTML = `
      <div class="email">No email messages.</div>
    `;
    // is there an ancient event listener?
    if (this.onload) 
      this.onerror(...);
    
    // there might be modern event listeners. dispatch an event.
    this.dispatchEvent(new Event('error'));
  

另请注意,我将您的 id 更改为 class,因为除非您编写一些奇怪的代码以仅允许页面上出现 &lt;e-mail&gt; 元素的单个实例,否则您不能使用唯一标识符并且然后将其分配给一堆元素。

【讨论】:

【参考方案7】:

我根据@Downgoat 的回答制作了这个测试用例。 它在 NodeJS 上运行。 这是 Downgoat 的代码,其中异步部分由 setTimeout() 调用提供。

'use strict';
const util = require( 'util' );

class AsyncConstructor

  constructor( lapse )
    this.qqq = 'QQQ';
    this.lapse = lapse;
    return ( async ( lapse ) => 
      await this.delay( lapse );
      return this;
    )( lapse );
  

  async delay(ms) 
    return await new Promise(resolve => setTimeout(resolve, ms));
  



let run = async ( millis ) => 
  // Instatiate with await, inside an async function
  let asyncConstructed = await new AsyncConstructor( millis );
  console.log( 'AsyncConstructor: ' + util.inspect( asyncConstructed ));
;

run( 777 );

我的用例是用于 Web 应用程序服务器端的 DAO。 正如我所看到的 DAO,它们每个都与记录格式相关联,在我的例子中是一个 MongoDB 集合,例如厨师。 CooksDAO 实例保存厨师的数据。 在我焦躁不安的头脑中,我将能够实例化厨师的 DAO,提供厨师 ID 作为参数,并且实例化将创建对象并用厨师的数据填充它。 因此需要在构造函数中运行异步的东西。 我想写:

let cook = new cooksDAO( '12345' );  

拥有cook.getDisplayName()等可用属性。 有了这个解决方案,我必须这样做:

let cook = await new cooksDAO( '12345' );  

这与理想非常相似。 此外,我需要在 async 函数中执行此操作。

我的 B 计划是将数据加载留在构造函数之外,基于 @slebetman 建议使用 init 函数,并执行以下操作:

let cook = new cooksDAO( '12345' );  
async cook.getData();

不违反规则。

【讨论】:

我已经修改了 Downgoat 的 answer。您能否检查一下这个答案现在是否提供了任何额外的东西,如果没有,请删除它?谢谢。【参考方案8】:

在构造函数中使用异步方法???

constructor(props) 
    super(props);
    (async () => await this.qwe(() => console.log(props), () => console.log(props)))();


async qwe(q, w) 
    return new Promise((rs, rj) => 
        rs(q());
        rj(w());
    );

【讨论】:

有人可以将其更改为有意义的变量名称吗? qweqwrsrj 是什么意思? 我猜queq 只是数据库上查询函数的占位符。 rsrj 代表来自 Promise 对象的 resolvereject:developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…【参考方案9】:

如果您可以避免extend,您可以完全避免使用类并使用函数组合作为构造函数。您可以使用范围内的变量而不是类成员:

async function buildA(...) 
  const data = await fetch(...);
  return 
    getData: function() 
      return data;
    
  

并简单地将其用作

const a = await buildA(...);

如果您使用的是 typescript 或 flow,您甚至可以强制执行 构造函数的接口

Interface A 
  getData: object;


async function buildA0(...): Promise<A>  ... 
async function buildA1(...): Promise<A>  ... 
...

【讨论】:

【参考方案10】:

我通常更喜欢返回新实例的静态异步方法,但这里有另一种方法。它更接近于字面上等待构造函数。它适用于 TypeScript。

class Foo 
  #promiseReady;

  constructor() 
    this.#promiseReady = this.#init();
  

  async #init() 
    await someAsyncStuff();
    return this;

  

  ready() 
    return this.promiseReady;
  

let foo = await new Foo().ready();

【讨论】:

【参考方案11】:

构建器模式的变体,使用 call():

function asyncMethod(arg) 
    function innerPromise()  return new Promise((...)=> ...) 
    innerPromise().then(result => 
        this.setStuff(result);
    


const getInstance = async (arg) => 
    let instance = new Instance();
    await asyncMethod.call(instance, arg);
    return instance;

【讨论】:

【参考方案12】:

您可以立即调用返回消息的匿名异步函数并将其设置为消息变量。如果您不熟悉此模式,您可能需要查看立即调用函数表达式 (IEFES)。这就像一个魅力。

var message = (async function()  return await grabUID(uid) )()

【讨论】:

IIFE 是 correct answer 的核心。【参考方案13】:

我发现自己处于这种情况并最终使用了 IIFE

// using TypeScript

class SomeClass 
    constructor() 
        // do something here
    

    doSomethingAsync(): SomeClass 
        (async () => await asyncTask())();
        return this;
    


const someClass = new SomeClass().doSomethingAsync();

如果您有其他任务依赖于异步任务,您可以在 IIFE 完成执行后运行它们。

【讨论】:

这几乎是the correct answer,它也使用了一个IIFE。【参考方案14】:

其他答案缺少明显的内容。只需从构造函数中调用异步函数:

constructor() 
    setContentAsync();


async setContentAsync() 
    let uid = this.getAttribute('data-uid')
    let message = await grabUID(uid)

    const shadowRoot = this.attachShadow(mode: 'open')
    shadowRoot.innerHTML = `
      <div id="email">A random email message has appeared. $message</div>
    `

【讨论】:

和another "obvious" answer here 一样,这个不会做程序员通常期望的构造函数,即在创建对象时设置内容。 @DanDascalescu 它是设置的,只是异步的,这正是提问者所需要的。您的意思是创建对象时内容没有同步设置,这不是问题所要求的。这就是为什么问题是关于在构造函数中使用 await/async 的原因。我已经演示了如何通过从构造函数调用异步函数来调用尽可能多的等待/异步。我已经完美地回答了这个问题。 @Navigateur 这与我想出的解决方案相同,但comments on another similar question 建议不应该这样做。构造函数中丢失了承诺的主要问题,这是反模式。您是否有任何参考文献推荐这种从构造函数调用异步函数的方法? @Marklar 没有参考资料,你为什么需要参考资料?如果您一开始就不需要某些东西,那么它是否“丢失”也没关系。如果您确实需要承诺,添加this.myPromise =(一般意义上的)是微不足道的,因此在任何意义上都不是反模式。有完全有效的情况需要在构造时启动一个本身没有返回值的异步算法,并且无论如何添加一个简单的我们,所以建议不要这样做的人是误解了一些事情 感谢您抽出宝贵时间回复。由于 *** 上的答案相互矛盾,我一直在寻找进一步的阅读材料。我希望确认这种情况下的一些最佳做法。【参考方案15】:

您应该将then 函数添加到实例。 Promise 会自动将其识别为带有 Promise.resolve 的 thenable 对象

const asyncSymbol = Symbol();
class MyClass 
    constructor() 
        this.asyncData = null
    
    then(resolve, reject) 
        return (this[asyncSymbol] = this[asyncSymbol] || new Promise((innerResolve, innerReject) => 
            this.asyncData =  a: 1 
            setTimeout(() => innerResolve(this.asyncData), 3000)
        )).then(resolve, reject)
    


async function wait() 
    const asyncData = await new MyClass();
    alert('run 3s later')
    alert(asyncData.a)

【讨论】:

innerResolve(this) 将不起作用,因为 this 仍然是一个 thenable。这导致了一个永无止境的递归解决方案。【参考方案16】:

@slebetmen 接受的答案很好地解释了为什么这不起作用。除了该答案中提出的两种模式之外,另一种选择是仅通过自定义异步 getter 访问您的异步属性。然后,constructor() 可以触发属性的异步创建,但 getter 会在使用或返回之前检查该属性是否可用。

当你想在启动时初始化一个全局对象,并且你想在一个模块中做它时,这种方法特别有用。无需在您的 index.js 中初始化并将实例传递到需要它的地方,只需在需要全局对象的地方 require 您的模块即可。

用法

const instance = new MyClass();
const prop = await instance.getMyProperty();

实施

class MyClass 
  constructor() 
    this.myProperty = null;
    this.myPropertyPromise = this.downloadAsyncStuff();
  
  async downloadAsyncStuff() 
    // await yourAsyncCall();
    this.myProperty = 'async property'; // this would instead by your async call
    return this.myProperty;
  
  getMyProperty() 
    if (this.myProperty) 
      return this.myProperty;
     else 
      return this.myPropertyPromise;
    
  

【讨论】:

@slebetman 的回答不正确,因为this answer 显示了异步构造函数的工作原理。【参考方案17】:

最接近异步构造函数的方法是等待它完成执行(如果它还没有在所有方法中执行):

class SomeClass 
    constructor() 
        this.asyncConstructor = (async () => 
            // Perform asynchronous operations here
        )()
    

    async someMethod() 
        await this.asyncConstructor
        // Perform normal logic here
    

【讨论】:

你可以通过actually using an async constructor更接近。

以上是关于异步/等待类构造函数的主要内容,如果未能解决你的问题,请参考以下文章

在构造函数或 ngOnInit 中加载异步函数

在 C# 中的类构造函数中调用异步方法 [重复]

javascript JavaScript异步类构造函数

在构造函数中进行 GWT 异步回调安全吗?

类构造函数中的ES6解构[重复]

扩展 Javascript 承诺并在构造函数中解决或拒绝它