异步/等待类构造函数

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

是否有一种方法来规避这一点,以便我可以使用异步/等待在这?而不是要求回调或.then()?

322839 次浏览

这可以从来没有工作。

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

你只能在可以使用承诺的地方使用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)();
}
}
  1. 使用构建器。我在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 () {}
}

根据您的注释,您可能应该做其他带有资产加载的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 {
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函数处理事件调用和shadow 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,因为除非你写了一些奇怪的代码,只允许一个页面上的<e-mail>元素的一个实例,否则你不能使用唯一标识符,然后将它分配给一堆元素。

你应该将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)
}

其他答案都忽略了一个显而易见的事实。简单地从你的构造函数调用一个async函数:

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

构建器模式的变体,使用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;
}

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

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

可以通过从构造函数返回立即调用的异步函数表达式明确地做到了这一点。IIAFE是一个非常常见的模式的花哨名称,在顶级等待可用之前,为了在async函数之外使用await,需要这个模式:

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

我们将使用此模式立即在构造函数中执行async函数,并将其结果返回为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. 使用异步构造函数扩展类会有限制。如果需要在派生类的构造函数中调用super,则必须在没有await的情况下调用它。如果你需要使用await调用超级构造函数,你会遇到TypeScript错误2337:Super calls are not permitted outside constructors or in nested functions inside constructors.
  2. 有人认为这是一种“不好的做法”。有一个构造函数返回承诺

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

@slebetmen的公认答案很好地解释了为什么这行不通。除了答案中给出的两种模式之外,另一种选择是仅通过自定义异步getter访问您的异步属性。然后构造函数()可以触发属性的异步创建,但是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;
}
}
}
我根据@Downgoat的答案做了这个测试用例 它运行在NodeJS上。 这是Downgoat的代码,其中async部分由setTimeout()调用提供。< / p >
'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实例保存cook的数据 在我不安的头脑中,我将能够实例化一个cook的DAO,提供cookId作为参数,实例化将创建对象并使用cook的数据填充它 因此需要在构造函数中运行async内容 我想写:

let cook = new cooksDAO( '12345' );

具有类似cook.getDisplayName()的可用属性 有了这个解决方案,我必须做:

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

,这与理想情况非常相似 另外,我需要在async函数中执行此操作。< / p >

我的b计划是把数据加载放在构造函数之外,基于@slebetman的建议使用init函数,并像这样做:

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

这并不违反规则。

在构造函数中使用async方法?

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

如果你可以避免 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> { ... }
...

您可以立即调用一个返回消息的匿名异步函数,并将其设置为message变量。如果您不熟悉这种模式,您可能想看看立即调用的函数表达式(IEFES)。这将非常有效。

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

权宜之计

你可以创建一个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),但这里我将使用extends,因为它有点符合子类语义(子类是父类+ extra,因为它们应该遵守父类的设计契约,并且可以做额外的事情;如果父类也不是异步的,异步子类会很奇怪,因为它不能以同样的方式初始化):


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

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}

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

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

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

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

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


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

我通常更喜欢返回一个新实例的静态异步方法,但这里有另一种方法。它更接近于字面上的等待构造函数。它与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();

我发现自己遇到了这样的情况,最终使用了IIFE

// using TypeScript


class SomeClass {
constructor() {
// do something here
}


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


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


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

这里有很多伟大的知识和一些超级()深思熟虑的回答。简而言之,下面概述的技术相当简单、非递归、异步兼容,并且遵循规则。更重要的是,我认为这里还没有正确地覆盖它-尽管如果错误请纠正我!

我们不调用方法,而是简单地将II(A)FE赋值给实例prop:

// it's async-lite!
class AsyncLiteComponent {
constructor() {
// our instance includes a 'ready' property: an IIAFE promise
// that auto-runs our async needs and then resolves to the instance
// ...
// this is the primary difference to other answers, in that we defer
// from a property, not a method, and the async functionality both
// auto-runs and the promise/prop resolves to the instance
this.ready = (async () => {
// in this example we're auto-fetching something
this.msg = await AsyncLiteComponent.msg;
// we return our instance to allow nifty one-liners (see below)
return this;
})();
}


// we keep our async functionality in a static async getter
// ... technically (with some minor tweaks), we could prefetch
// or cache this response (but that isn't really our goal here)
static get msg() {
// yes I know - this example returns almost immediately (imagination people!)
return fetch('data:,Hello%20World%21').then((e) => e.text());
}
}

看起来很简单,怎么用呢?

// Ok, so you *could* instantiate it the normal, excessively boring way
const iwillnotwait = new AsyncLiteComponent();
// and defer your waiting for later
await iwillnotwait.ready
console.log(iwillnotwait.msg)


// OR OR OR you can get all async/awaity about it!
const onlywhenimready = await new AsyncLiteComponent().ready;
console.log(onlywhenimready.msg)


// ... if you're really antsy you could even "pre-wait" using the static method,
// but you'd probably want some caching / update logic in the class first
const prefetched = await AsyncLiteComponent.msg;


// ... and I haven't fully tested this but it should also be open for extension
class Extensior extends AsyncLiteComponent {
constructor() {
super();
this.ready.then(() => console.log(this.msg))
}
}
const extendedwaittime = await new Extensior().ready;


在发帖之前,我在 @slebetman的综合回答的评论中对这种技术的可行性进行了简要的讨论。我并没有完全被这种直接的解雇所说服,所以我认为我应该进一步讨论/推翻它。请尽你最大的努力吧。

你可以使用Proxy的construct句柄来做到这一点,代码如下:

const SomeClass = new Proxy(class A {
constructor(user) {
this.user = user;
}
}, {
async construct(target, args, newTarget) {
const [name] = args;
// you can use await in here
const user = await fetch(name);
// invoke new A here
return new target(user);
}
});


const a = await new SomeClass('cage');
console.log(a.user); // user info