异步/等待类构造函数


169

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

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

有没有办法避免这种情况,以便我可以在其中使用异步/等待?而不是需要回调或.then()?


6
构造函数的目的是为您分配一个对象,然后立即返回。您能否更具体地说明为什么您认为构造函数应该异步?因为我们几乎可以保证在这里处理XY问题
Mike'Pomax'Kamermans

4
@ Mike'Pomax'Kamermans很有可能。基本上,我需要查询数据库以获取加载此元素所需的元数据。查询数据库是异步操作,因此在构造元素之前,我需要某种方式来等待数据库完成。我宁愿不使用回调,因为在整个项目的其余部分都使用了await / async并希望保持连续性。
亚历山大·克拉格斯

@ Mike'Pomax'Kamermans的完整上下文是一个电子邮件客户端,其中,每个HTML元素看上去<e-mail data-uid="1028"></email>都与之相似,并且使用该customElements.define()方法从中填充信息。
亚历山大·克拉格斯

您几乎不希望构造函数异步。创建一个同步构造函数,该构造函数返回您的对象,然后使用类似的方法.init()进行异步处理。另外,由于您是HTMLElement的子手,因此使用此类的代码很可能不知道这是异步的事情,因此无论如何您都必须寻找一种完全不同的解决方案。
jfriend00

Answers:


262

永远行不通。

async关键字允许await在标记为函数中使用async,但它也是功能转换成一个承诺发生器。因此,标有的函数async将返回承诺。另一方面,构造函数返回正在构造的对象。因此,在这种情况下,您既要返回对象又要兑现承诺:一种不可能的情况。

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

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

  1. 使用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)();
        }
    }
  2. 使用一个生成器。我没有看到它在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 () {}
}

请注意,基于注释,我们的想法是这是一个html元素,通常没有手册,init()但是功能绑定到某些特定属性,例如srchref(在这种情况下为data-uid),这意味着使用设置器每次绑定一个新值时,都绑定并启动init(也可能在构造过程中,但是当然不必等待生成的代码路径)
Mike'Pomax'Kamermans

您应该评论以下答案不足的原因(如果有)。或以其他方式解决。
奥吉·加德纳

我很好奇为什么bind第一个示例中需要这样做callback.bind(this)();?这样您就可以this.otherFunc()在回调中进行操作?
亚历山大·克拉格斯

1
@AlexanderCraggs只是很方便,所以this在回调中指向myClass。如果您始终使用myObjthis不是不需要它
slebetman

1
当前是对语言的限制,但我看不出为什么将来不能const a = await new A()像我们有常规函数和异步函数一样使用。
7ynk3r

138

您绝对可以做到这一点。基本上:

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

            // All async code here
            this.value = await asyncFunction();

            return this; // when done
        })();
    }
}

创建类使用:

let instance = await new AsyncConstructor();

但是,此解决方案有一些不足之处:

super注意:如果需要使用super,则不能在异步回调中调用它。

TypeScript注意:这会导致TypeScript问题,因为构造函数返回type Promise<MyClass>而不是MyClass。据我所知,没有确定的方法可以解决此问题。@blitter建议的一种可能方式是放在/** @type {any} */构造函数主体的开头,但是我不知道这是否在所有情况下都有效。


1
@PAStheLoD我认为它不会在没有返回的情况下解析为该对象,但是您说的是这样,所以我将审查规格并进行更新...
Downgoat

2
@JuanLanus异步块将自动捕获参数,因此对于参数x您只需要做constructor(x) { return (async()=>{await f(x); return this})() }
Downgoat

1
@PAStheLoD:return this是必需的,因为虽然constructor它会自动为您执行,但异步IIFE不需要,并且您最终将返回一个空对象。
Dan Dascalescu

1
当前从针对ES5,ES2017,ES2018(可能还有其他,但我尚未检查)的TS 3.5.1开始,如果在构造函数中进行返回,则会收到以下错误消息:“构造函数签名的返回类型必须可分配给类的实例类型。” IIFE的类型是Promise <this>,并且由于该类不是Promise <T>,所以我看不到它如何工作。(除了“ this”,您还能返回什么?)因此,这两个返回都是不必要的。(最外面的会更糟,因为它会导致编译错误。)
PAStheLoD

3
@PAStheLoD是的,这是一个打字稿限制。通常,在JS中,一个类在构造T时应返回T,但要获得我们返回的异步能力,该能力Promise<T>解析为this,但会混淆打字稿。您确实需要外部返回,否则您将不知道诺言何时完成—结果是这种方法无法在TypeScript上使用(除非有一些可能使用类型别名的技巧?)。虽然不是打字专家,但无法这样说
Downgoat

7

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

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()从一个异步函数。


5

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

是的,这意味着使用诺言,但同时也意味着“以与其他所有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 {
  constructor() {
    super();
    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
    let getEmail = new Promise( (resolve, reject) => {
      yourDataBase.getByUID(uid, (err, result) => {
        if (err) return reject(err);
        resolve(result);
      });
    });
    // kick off the promise, which will be async all on its own
    getEmail()
    .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', ...));
  }

还要注意,我将您更改idclass,因为除非您编写一些怪异的代码以仅允许<e-mail>页面上元素的单个实例,否则您将无法使用唯一标识符,然后将其分配给一堆元素。


2

我根据@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集合,例如Cook。
cooksDAO实例保存厨师的数据。
在我烦躁不安的头脑中,我将能够实例化厨师的DAO,并提供cookId作为参数,实例化将创建对象并用厨师的数据填充该对象。
因此,需要在构造函数中运行异步内容。
我想写:

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

这不会违反规则。


2

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

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

2

权宜之计

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

这并不干净,因为它依赖于使用您的代码的每个人以及您自己,始终像这样实例化该对象。但是,如果仅在代码中的一个或两个特定位置使用此对象,则可能会很好。

但是,由于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}

但是,说您想在许多对象上使用此模式...可以将其抽象为装饰器,或者在定义like之后调用(俗称ugh)它postProcess_makeAsyncInit(AsyncOnlyObject),但是在这里我将使用extends它,因为它适合子类语义(子类是父类+其他子类,因为它们应遵守父类的设计约定,并且可能会做其他事情;如果父类也不是异步的,那么异步子类也会很奇怪,因为无法将其初始化为同一个子类方式):


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

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}

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


2

与其他人所说的不同,您可以使其正常工作。

JavaScript class可以从其字面上返回任何内容constructor,甚至可以返回另一个类的实例。因此,您可能会返回一个Promise从类的构造函数解析为其实际实例的a。

下面是一个示例:

export class Foo {

    constructor() {

        return (async () => {

            // await anything you want

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

然后,您将以Foo这种方式创建实例:

const foo = await new Foo();

1

如果可以避免 extend,则可以避免所有类,而将函数组合用作构造函数。您可以在范围中使用变量而不是类成员:

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

并简单地用作

const a = await buildA(...);

如果您使用的是打字稿或流,您甚至可以强制执行构造函数的接口

Interface A {
  getData: object;
}

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

0

使用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;
}

0

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

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

-1

@slebetmen接受的答案很好地解释了为什么这行不通。除了该答案中介绍的两种模式之外,另一种选择是仅通过自定义异步获取器访问异步属性。然后,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;
    }
  }
}

-2

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

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>
    `
}

就像这里的另一个“显而易见”的答案一样,这个答案不会像程序员通常期望的那样构造函数,即在创建对象时设置内容。
Dan Dascalescu

2
@DanDascalescu它是异步设置的,这正是发问者所需要的。您的观点是,创建对象时不会同步设置内容,这不是问题所必需的。这就是为什么问题是关于在构造函数中使用await / async的原因。我已经演示了如何通过从构造函数调用异步函数来从构造函数中调用任意数量的等待/异步。我已经完美地回答了这个问题。
Navigateur

@Navigateur与我想出的解决方案相同,但是对另一个类似问题评论表明,不应以这种方式进行处理。承诺的主要问题在构造函数中丢失,这是反模式。您是否有任何参考文献推荐这种从构造函数调用异步函数的方法?
Marklar '18 -10-25

1
@Marklar没有参考,为什么需要任何参考?如果您一开始不需要某些东西,那么“丢失”就没有关系。而且,如果您确实需要诺言,那么添加this.myPromise =(一般意义上)很简单,因此无论如何都不添加反模式。有完全有效的情况,需要在构造时启动异步算法,该算法本身没有返回值,并且无论如何添加一个简单的方法,因此,建议不要这样做的人会误解某些内容
Navigateur

1
感谢您抽出时间回复。我正在寻找进一步的阅读资料,因为此处Stackoverflow上的答案相互矛盾。我希望确认这种情况下的一些最佳实践。
Marklar '18 -10-29

-2

您应该then向实例添加功能。PromisePromise.resolve自动将其识别为可行的对象

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仍然无法正常工作。这导致了永无止境的递归解析。
贝吉
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.