异步/等待类构造函数缺点

2022-08-29 23:52:05

目前,我正在尝试在类构造函数中使用。这样我就可以为我正在做的Electron项目获得一个自定义标签。async/awaite-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

有没有办法绕过这个问题,以便我可以在其中使用异步/等待?而不是要求回调或 .then()?


答案 1

永远行不通。

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

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

有两种设计模式可以克服这一点,这两种模式都是在承诺出现之前发明的。

  1. 函数的使用。这有点像jQuery的。您创建的对象只能在其自身或函数内部使用:init().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);
               });
        }
    }
    

    使用异步/等待实现:

    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,但严格来说,它们并不是必需的。您可以轻松编写接受回调的构建器。


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

这与异步构造函数没有任何关系,而是与关键字的实际含义有关(对于来自自动解析方法名称的语言(即不需要关键字的语言)的人来说,这可能有点令人惊讶)。thisthis

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

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

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

答案 2

您绝对可以通过从构造函数返回立即调用的异步函数表达式来执行此操作。 是一个非常常见的模式的花哨名称,在顶级 await 可用之前,为了在异步函数之外使用而需要它:IIAFEawait

(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,您需要断言构造函数的类型是类类型,而不是返回类类型的 promise:

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

缺点

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

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