277 votes

Async / Await Class Constructor

Pour le moment, j'essaie d'utiliser async/await dans une fonction du constructeur de classe. Cela me permet d’obtenir une étiquette personnalisée e-mail pour un projet Electron sur lequel je travaille.

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

Pour le moment cependant, le projet ne fonctionne pas, avec l'erreur suivante:

 Class constructor may not be an async method
 

Y at-il un moyen de contourner cela afin que je puisse utiliser async / wait dans cela? Au lieu d'exiger des rappels ou .then ()?

394voto

slebetman Points 28276

Cela peut ne jamais le travail.

L' async mot-clé permet d' await à être utilisé dans une fonction marquée async , mais il convertit également cette fonction dans une promesse générateur. Donc une fonction marquée avec async sera de retour d'une promesse. Un constructeur d'autre part renvoie à l'objet qu'il est en train de construire. Ainsi, nous avons une situation où vous voulez retourner un objet et une promesse: une situation impossible.

Vous ne pouvez utiliser async/await où vous pouvez utiliser des promesses, car ils sont essentiellement syntaxe de sucre pour des promesses. Vous ne pouvez pas utiliser des promesses dans un constructeur parce qu'un constructeur doit renvoyer l'objet à construire, pas une promesse.

Il existe deux modèles de conception pour surmonter cette difficulté, à la fois inventé avant promesses ont été autour.

  1. L'utilisation d'un init() fonction. Cela fonctionne un peu comme du jQuery .ready(). L'objet que vous créez peut être utilisé uniquement à l'intérieur de sa propre init ou ready fonction de:

    Utilisation:

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

    Mise en œuvre:

    class myClass {
        constructor () {
    
        }
    
        init (callback) {
            // do something async and call the callback:
            callback.bind(this)();
        }
    }
    
  2. L'utilisation d'un constructeur. Je n'ai pas vu ce très utilisée en javascript, mais c'est l'un des plus commun des solutions de rechange en Java lorsqu'un objet doit être construit de manière asynchrone. Bien sûr, le générateur de modèle est utilisé lors de la construction d'un objet qui nécessite beaucoup de compliqué paramètres. Ce qui est exactement le cas d'utilisation asynchrone constructeurs. La différence est que d'un générateur asynchrone ne renvoie pas à un objet, mais une promesse de cet objet:

    Utilisation:

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

    Mise en œuvre:

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

    Mise en œuvre avec 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);
        }
    }
    

Remarque: bien que dans les exemples ci-dessus, nous utilisons des promesses pour le générateur asynchrone ils ne sont pas à proprement parler nécessaire. Vous pouvez tout aussi bien écrire un constructeur qui accepte un rappel.


Note sur les appels de fonctions à l'intérieur des fonctions statiques.

Cela n'a rien à voir avec async constructeurs, mais avec le mot-clé this signifie réellement (qui peut être un peu surprenant, les personnes en provenance des langues qui n'automatique de la résolution de noms de méthode, qui est, les langues qui n'ont pas besoin de l' this mot-clé).

L' this mot-clé renvoie à l'objet instancié. Pas la classe. Par conséquent, vous ne peut normalement pas utiliser this à l'intérieur des fonctions statiques puisque la fonction statique n'est lié à aucun objet, mais est liée directement à la classe.

C'est-à-dire, dans le code suivant:

class A {
    static foo () {}
}

Vous ne pouvez pas faire:

var a = new A();
a.foo() // NOPE!!

au lieu de cela, vous devez l'appeler comme:

A.foo();

Par conséquent, le code suivant devrait en résulter une erreur:

class A {
    static foo () {
        this.bar(); // you are calling this as static
                    // so bar is undefinned
    }
    bar () {}
}

Pour corriger cela, vous pouvez faire bar soit une fonction régulière ou d'une méthode statique:

function bar1 () {}

class A {
    static foo () {
        bar1();   // this is OK
        A.bar2(); // this is OK
    }

    static bar2 () {}
}

223voto

Downgoat Points 4277

Vous pouvez certainement le faire. Fondamentalement:

 class AsyncConstructor {
    constructor() {
        return (async () => {

            // All async code here
            this.value = await asyncFunction();

            return this; // when done
        })();
    }
}
 

pour créer la classe utiliser:

 let instance = await new AsyncConstructor();
 

Remarque: Si vous devez utiliser super, vous ne pouvez pas l'appeler dans le rappel asynchrone. Vous devez l'appeler en dehors de celui-ci pour que cette solution ne soit pas parfaite à 100%, mais à mon avis, elle est plutôt idiomatique et je l'utilise tout le temps dans mon code.

14voto

Vidar Points 334

Comme les fonctions asynchrones sont des promesses, vous pouvez créer une fonction statique sur votre classe qui exécute une fonction asynchrone qui renvoie l'instance de la classe:

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

Appelez avec let yql = await Yql.init() depuis une fonction asynchrone.

5voto

En fonction de vos commentaires, vous devriez probablement faire ce que tous les autres HTMLElement avec des actifs de chargement: faites le constructeur de commencer un téléchargement local d'action, générant une charge ou un événement d'erreur en fonction du résultat.

Oui, cela signifie que l'aide de promesses, mais il signifie aussi "faire les choses de la même manière que tout autre élément HTML", de sorte que vous êtes en bonne compagnie. Par exemple:

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

ce coup d'envoi asynchrone de charge de la source de l'actif qui, lorsqu'elle réussit, se termine en onload et quand ça se passe mal, se termine en onerror. Donc, faire votre propre classe de le faire aussi:

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

Et puis vous faites le renderLoaded/renderError fonctions de traiter le cas des appels d'ombre et de 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', ...));
  }

Notez aussi j'ai changé d' id d'un class, parce que si vous écrivez un peu bizarre de code pour ne jamais permettre à une instance unique de votre <e-mail> élément sur une page, vous ne pouvez pas utiliser un identifiant unique et l'attribuer à un tas d'éléments.

2voto

Juan Lanus Points 677

J'ai fait ce test basé sur @Downgoat de réponse.
Il fonctionne sur NodeJS. C'est Downgoat du code où l'async partie est fournie par un setTimeout() appel.

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

Mon cas d'utilisation est DAOs pour le côté serveur d'une application web.
Comme je vois les DAOs, ils sont chacun associés à un format d'enregistrement, dans mon cas, une collection MongoDB comme par exemple, un cuisinier.
Un cooksDAO instance est titulaire d'un cap de cuisinier de données.
Dans mon esprit agité, je serais en mesure d'instancier un cuisinier du DAO de fournir le cookId comme argument, et l'instanciation serait de créer l'objet et de le remplir avec le cuisinier de données.
Ainsi, la nécessité d'exécuter async des trucs dans le constructeur.
Je voulais écrire:

let cook = new cooksDAO( '12345' );  

pour disposer de propriétés comme l' cook.getDisplayName().
Avec cette solution j'ai à faire:

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

ce qui est très similaire à l'idéal.
Aussi, j'ai besoin de faire cela à l'intérieur d'un async fonction.

Ma B-plan de a été de laisser le chargement des données de l'constructeur, basé sur @slebetman suggestion pour l'utilisation d'une fonction init, et faire quelque chose comme ceci:

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

ce qui ne veut pas briser les règles.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X