目前,我正在尝试在类构造函数中使用async/await。这样我就可以为我正在进行的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()?


当前回答

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

当你想在启动后初始化一个全局对象,并且你想在一个模块中做这件事时,这种方法特别有用。而不是在index.js中初始化并将实例传递到需要它的地方,只需在需要全局对象的地方调用模块。

使用

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

其他回答

构建器模式的变体,使用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函数:

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

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

权宜之计

你可以创建一个async init(){…返回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}

However say you want to use this pattern on many objects... you could abstract this as a decorator or something you (verbosely, ugh) call after defining like postProcess_makeAsyncInit(AsyncOnlyObject), but here I'm going to use extends because it sort of fits into subclass semantics (subclasses are parent class + extra, in that they should obey the design contract of the parent class, and may do additional things; an async subclass would be strange if the parent wasn't also async, because it could not be initialized the same way):


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

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}

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

根据您的注释,您可能应该做其他带有资产加载的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更改为一个类,因为除非您编写一些奇怪的代码,只允许页面上的<e-mail>元素的单个实例,否则您不能使用唯一标识符,然后将其分配给一堆元素。