Async/Await Class Constructor

匿名 (未验证) 提交于 2019-12-03 01:27:01

问题:

At the moment, I'm attempting to use async/await within a class constructor function. This is so that I can get a custom e-mail tag for an Electron project I'm working on.

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 = `       
A random email message has appeared. ${message}
` } })

At the moment however, the project does not work, with the following error:

Class constructor may not be an async method 

Is there a way to circumvent this so that I can use async/await within this? Instead of requiring callbacks or .then()?

回答1:

This can never work.

The async keyword allows await to be used in a function marked as async but it also converts that function into a promise generator. So a function marked with async will return a promise. A constructor on the other hand returns the object it is constructing. Thus we have a situation where you want to both return an object and a promise: an impossible situation.

You can only use async/await where you can use promises because they are essentially syntax sugar for promises. You can't use promises in a constructor because a constructor must return the object to be constructed, not a promise.

There are two design patterns to overcome this, both invented before promises were around.

  1. Use of an init() function. This works a bit like jQuery's .ready(). The object you create can only be used inside it's own init or ready function:

    Usage:

    var myObj = new myClass(); myObj.init(function() {     // inside here you can use myObj }); 

    Implementation:

    class myClass {     constructor () {      }      init (callback) {         // do something async and call the callback:         callback.bind(this)();     } } 
  2. Use a builder. I've not seen this used much in javascript but this is one of the more common work-arounds in Java when an object needs to be constructed asynchronously. Of course, the builder pattern is used when constructing an object that requires a lot of complicated parameters. Which is exactly the use-case for asynchronous builders. The difference is that an async builder does not return an object but a promise of that object:

    Usage:

    myClass.build().then(function(myObj) {     // myObj is returned by the promise,      // not by the constructor     // or builder }); 

    Implementation:

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

    Implementation with 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);     } } 

Note: although in the examples above we use promises for the async builder they are not strictly speaking necessary. You can just as easily write a builder that accept a callback.



回答2:

Based on your comments, you should probably do what every other HTMLElement with asset loading does: make the constructor start a sideloading action, generating a load or error event depending on the result.

Yes, that means using promises, but it also means "doing things the same way as every other HTML element", so you're in good company. For instance:

var img = new Image(); img.onload = function(evt) { ... } img.addEventListener("load", evt => ... ); img.onerror = function(evt) { ... } img.addEventListener("error", evt => ... ); img.src = "some url"; 

this kicks off an asynchronous load of the source asset that, when it succeeds, ends in onload and when it goes wrong, ends in onerror. So, make your own class do this too:

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

And then you make the renderLoaded/renderError functions deal with the event calls and shadow dom:

  renderLoaded(message) {     const shadowRoot = this.attachShadow({mode: 'open'});     shadowRoot.innerHTML = `            `;     // 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 = `            `;     // 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', ...));   } 

Also note I changed your id to a class, because unless you write some weird code to only ever allow a single instance of your element on a page, you can't use a unique identifier and then assign it to a bunch of elements.



回答3:

You can also create a self-executing async anonymous function in your constructor:

class MyClass {     constructor() {         (async () => {             let result = await foo();             this.someProperty = result;             console.log(this.someProperty);// outputs: bar         })();     } }  function foo() {     return new Promise((resolve, reject) => {         setTimeout(() => resolve('bar'), 2000);     }); }  new MyClass(); 


回答4:

The other answers are missing the obvious. Simply call an async function from your constructor:

constructor() {     setContentAsync(); }  async setContentAsync() {     let uid = this.getAttribute('data-uid')     let message = await grabUID(uid)      const shadowRoot = this.attachShadow({mode: 'open'})     shadowRoot.innerHTML = `       
A random email message has appeared. ${message}
` }


回答5:

You should add then function to instance. Promise will recognize it as a thenable object with Promise.resolve automatically

const asyncSymbol = Symbol(); class MyClass {     constructor() {     // nothing to do with constructor     }     then(resolve, reject) {         return (this[asyncSymbol] = this[asyncSymbol] || new Promise((innerResolve, innerReject) => {             setTimeout(() => innerResolve(this), 3000)         })).then(resolve, reject)     } }  async function wait() {     const myInstance = await new MyClass();     alert('run 3s later') } 


标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!