web-dev-qa-db-fra.com

Constructeur de classe asynchrone/attente

Pour le moment, j'essaie d'utiliser async/await au sein d'une fonction constructeur de classe. Ceci afin de pouvoir obtenir une balise e-mail personnalisée 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 de demander des rappels ou .then ()?

58
Alexander Craggs

Cela peut jamais fonctionne.

Le mot clé async permet à await d'être utilisé dans une fonction marquée comme async, mais il convertit également cette fonction en un générateur de promesse. Donc, une fonction marquée avec async retournera une promesse. D'autre part, un constructeur renvoie l'objet qu'il est en train de construire. Nous avons donc une situation dans laquelle vous souhaitez à la fois retourner un objet et une promesse: une situation impossible.

Vous pouvez uniquement utiliser async/wait où vous pouvez utiliser des promesses, car elles sont essentiellement du sucre syntaxique pour les promesses. Vous ne pouvez pas utiliser de promesses dans un constructeur car un constructeur doit renvoyer l'objet à construire, et non une promesse.

Il existe deux modèles de conception pour surmonter cela, tous deux inventés avant que les promesses ne soient faites.

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

    Usage:

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

    La mise en oeuvre:

    class myClass {
        constructor () {
    
        }
    
        init (callback) {
            // do something async and call the callback:
            callback.bind(this)();
        }
    }
    
  2. Utilisez un constructeur. Je n'ai pas vu cela beaucoup utiliser en javascript, mais c'est l'une des solutions les plus courantes en Java lorsqu'un objet doit être construit de manière asynchrone. Bien entendu, le modèle de générateur est utilisé lors de la construction d'un objet nécessitant de nombreux paramètres complexes. Ce qui est exactement le cas d'utilisation pour les générateurs asynchrones. La différence est qu'un générateur asynchrone ne retourne pas un objet mais une promesse de cet objet:

    Usage:

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

    La mise en oeuvre:

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

    Implémentation avec async/wait:

    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 utilisions des promesses pour le constructeur async, elles ne sont pas à proprement parler nécessaires. Vous pouvez tout aussi facilement écrire un générateur qui accepte un rappel.


Note sur l'appel de fonctions à l'intérieur de fonctions statiques.

Cela n'a rien à voir avec les constructeurs asynchrones, mais avec la signification réelle du mot clé this (ce qui peut surprendre les personnes issues de langages utilisant la résolution automatique des noms de méthodes, c'est-à-dire qui n'ont pas besoin du mot clé this ).

Le mot clé this fait référence à l'objet instancié. Pas la classe. Par conséquent, vous ne pouvez pas utiliser normalement this dans les fonctions statiques, car la fonction statique n'est liée à aucun objet, mais directement à la classe.

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

class A {
    static foo () {}
}

Tu ne peux pas faire:

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

à la place, vous devez l'appeler comme suit:

A.foo();

Par conséquent, le code suivant entraînerait une erreur:

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

Pour résoudre ce problème, vous pouvez faire bar soit une fonction régulière, soit une méthode statique:

function bar1 () {}

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

    static bar2 () {}
}
104
slebetman

Vous pouvez faites certainement ceci. 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 soit pas 100% parfaite, mais à mon avis, elle est plutôt idiomatique et je l'utilise tout le temps dans mon code.

31
Downgoat

En fonction de vos commentaires, vous devriez probablement faire comme tous les autres HTMLElement avec le chargement d'actifs: obliger le constructeur à lancer une action de chargement parallèle, générant un événement load ou error en fonction du résultat.

Oui, cela signifie utiliser des promesses, mais cela signifie également "faire les choses de la même manière que tout autre élément HTML", vous êtes donc 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";

cela déclenche un chargement asynchrone de l'actif source qui, lorsqu'il réussit, se termine par onload et, lorsqu'il échoue, se termine par onerror. Alors, faites votre propre classe faire cela 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 ensuite, vous assurez que les fonctions renderLoaded/renderError traitent les appels d’événements et le dom shadow:

  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 également que j'ai changé votre id en class car, à moins d'écrire un code étrange pour n'autoriser qu'une seule instance de votre élément <e-mail> sur une page, vous ne pouvez pas utiliser un identificateur unique, puis l'affecter à un groupe d'éléments.

4

Les fonctions asynchrones étant 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.

1
Vidar

Si vous pouvez éviterextend, vous pouvez éviter toutes les classes et utiliser la composition de fonctions comme constructeurs . Vous pouvez utiliser les variables de l'étendue au lieu des membres de la classe:

async function buildA(...) {
  const data = await fetch(...);
  return {
    getData: function() {
      return data;
    }
  }
}

et simple l'utiliser comme

const a = await buildA(...);

Si vous utilisez TypeScript ou flow, vous pouvez même appliquer l'interface des constructeurs

Interface A {
  getData: object;
}

async function buildA0(...): Promise<A> { ... }
async function buildA1(...): Promise<A> { ... }
...
1
7ynk3r

utiliser la méthode async dans la construction ???

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

J'ai réalisé ce test basé sur la réponse de @ Downgoat.
Il tourne sur NodeJS. Ceci est le code de Downgoat où la partie async est fournie par un appel setTimeout()

'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 DAO pour le côté serveur d'une application Web.
Comme je vois les DAO, ils sont chacun associés à un format d'enregistrement, dans mon cas une collection MongoDB comme par exemple un cuisinier.
Une instance de cooksDAO contient les données d'un cuisinier.
Dans mon esprit agité, je serais capable d'instancier un DAO d'un cuisinier en fournissant le cookId en tant qu'argument, l'instanciation créerait l'objet et le remplirait avec les données du cuisinier.
Ainsi, il est nécessaire d’exécuter des commandes asynchrones dans le constructeur.
Je voulais écrire: 

let cook = new cooksDAO( '12345' );  

d'avoir des propriétés disponibles comme cook.getDisplayName().
Avec cette solution je dois faire: 

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

ce qui est très semblable à l'idéal.
De plus, je dois le faire dans une fonction async

Mon plan B consistait à laisser le chargement de données en dehors du constructeur, en fonction de la suggestion de @slebetman d'utiliser une fonction init, et à faire quelque chose comme ceci: 

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

ce qui n'enfreint pas les règles. 

0
Juan Lanus

Variation sur le modèle de générateur, à l'aide de 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;
}
0
Jeff Lowery

Vous pouvez immédiatement appeler une fonction asynchrone anonyme qui renvoie un message et le définir comme variable de message. Vous voudrez peut-être jeter un coup d'œil aux expressions de fonction invoquées immédiatement (IEFES), au cas où vous ne maîtriseriez pas ce modèle. Cela fonctionnera comme un charme.

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