异步/等待类构造函数

IT技术 javascript node.js async-await
2021-01-27 12:04:42

目前,我正在尝试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

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

6个回答

永远行不通。

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

您只能在可以使用 promise 的地方使用 async/await,因为它们本质上是 promise 的语法糖。不能在构造函数中使用 promise,因为构造函数必须返回要构造的对象,而不是 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 中更常见的解决方法之一。当然,构建器模式是在构造需要大量复杂参数的对象时使用的。这正是异步构建器的用例。不同之处在于异步构建器不返回对象而是该对象的Promise:

    用法:

    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关键字是指实例化的对象。不是class。因此您通常不能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),这意味着使用 setter每次绑定新值时绑定并启动 init(也可能在构造过程中,但当然无需等待生成的代码路径)
2021-03-26 12:04:42
您应该评论为什么下面的答案是不够的(如果是)。或者以其他方式解决它。
2021-03-28 12:04:42
目前是语言的限制,但我不明白为什么将来你不能const a = await new A()像我们拥有常规函数和异步函数那样拥有。
2021-03-31 12:04:42
@AlexanderCraggs 这只是方便,所以this在回调中引用myClass. 如果你总是使用myObj而不是this你不需要它
2021-04-08 12:04:42
我不明白这是绝对不可能的。异步函数最终仍会返回,只是被推迟了。异步函数可以像普通函数一样愉快地返回,你只需要等待它。没有根本的不匹配。正如你在下面看到的,有人已经解决了它。
2021-04-10 12:04:42

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

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 出现问题,因为构造函数返回类型Promise<MyClass>而不是MyClass. 据我所知,没有确定的方法来解决这个问题。@blitter 建议的一种可能的方法是放在/** @type {any} */构造函数体的开头——但是我不知道这是否适用于所有情况。

目前针对 ES5、ES2017、ES2018(可能还有其他的,但我还没有检查过)的 TS 3.5.1 如果您在构造函数中执行返回,则会收到此错误消息:“构造函数签名的返回类型必须可分配给类的实例类型。” IIFE 的类型是 Promise<this>,并且由于该类不是 Promise<T>,我不知道它是如何工作的。(除了“this”你还能返回什么?)所以这意味着两个返回都是不必要的。(外面的有点糟糕,因为它会导致编译错误。)
2021-03-22 12:04:42
@PAStheLoD 我认为它不会在没有返回的情况下解析为对象,但是您说它确实如此,因此我将查看规范并更新...
2021-03-28 12:04:42
@PAStheLoD 是的,这是typescript限制。通常在 JS 中,一个类T应该T在构造时返回,但为了获得我们返回的异步能力,Promise<T>它解析为this,但这会混淆typescript。您确实需要外部返回,否则您将不知道 Promise 何时完成——因此这种方法在 TypeScript 上不起作用(除非有一些可能带有类型别名的 hack?)。虽然不是打字专家,所以不能说
2021-04-02 12:04:42
@JuanLanus 异步块将自动捕获参数,因此对于参数 x,您只需要执行 constructor(x) { return (async()=>{await f(x); return this})() }
2021-04-09 12:04:42
@PAStheLoD:return this是必要的,因为 whileconstructor会自动为您执行,而异步 IIFE 则不会,您最终会返回一个空对象。
2021-04-09 12:04:42

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

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

与其他人所说的不同,您可以让它发挥作用。

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

下面是一个例子:

export class Foo {

    constructor() {

        return (async () => {

            // await anything you want

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

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

const foo = await new Foo();
的参数call被忽略,因为它是一个箭头函数。
2021-03-12 12:04:42
好像和Downgoat比你早半年回答的一样我不认为重新发布相同的想法有什么意义。
2021-03-18 12:04:42
你是对的,@罗伯特。这都是我的错。稍后我会更新我的答案——用.call(this)正常的函数调用替换调用应该没问题。感谢您指出了这一点
2021-03-25 12:04:42
对不起,@trincot,把同样的事情发过来了。写我的时,我没有注意到 Downgoat 的回答。我希望我的回答不会弄乱整个 StackOverflow 问题;) 我尊重你的反对票,即使我不完全理解它对社区有什么帮助,因为我的行为不是故意的。
2021-03-27 12:04:42

权宜之计

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

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

一个严重的问题发生了,因为 ES 没有类型系统,所以如果你忘记调用它,你只是返回,undefined因为构造函数什么都不返回。oop。更好的是做这样的事情:

最好的做法是:

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 后(详细地,呃)调用的东西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}

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