wake-up-neo.com

Klassenkonstruktor für Async/Await

Im Moment versuche ich, async/await innerhalb einer Klassenkonstruktorfunktion zu verwenden. Damit kann ich ein benutzerdefiniertes e-mail-Tag für ein Electron-Projekt erhalten, an dem ich gerade arbeite.

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

Momentan funktioniert das Projekt jedoch nicht mit dem folgenden Fehler:

Class constructor may not be an async method

Gibt es eine Möglichkeit, dies zu umgehen, damit ich async/await innerhalb dieses verwenden kann? Anstatt Callbacks oder .then () zu benötigen?

58

Das kann niemals funktionieren.

Mit dem Schlüsselwort async kann await in einer als async markierten Funktion verwendet werden, es wird jedoch auch diese Funktion in einen Versprechen-Generator umgewandelt. Eine mit async markierte Funktion gibt also ein Versprechen zurück. Ein Konstruktor hingegen gibt das Objekt zurück, das er erstellt. Wir haben also eine Situation, in der Sie sowohl einen Gegenstand als auch ein Versprechen zurückgeben möchten: eine unmögliche Situation.

Sie können async/await nur dort verwenden, wo Sie Versprechen verwenden können, da sie im Wesentlichen Syntaxzucker für Versprechen sind. Sie können Versprechen nicht in einem Konstruktor verwenden, da ein Konstruktor das zu erstellende Objekt zurückgeben muss, kein Versprechen.

Um dies zu überwinden, gibt es zwei Designmuster, die beide erfunden wurden, bevor die Versprechen in der Nähe waren.

  1. Verwendung einer init()-Funktion. Dies funktioniert ein bisschen wie .ready() von jQuery. Das von Ihnen erstellte Objekt kann nur innerhalb seiner eigenen init- oder ready-Funktion verwendet werden:

    Verwendungszweck:

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

    Implementierung:

    class myClass {
        constructor () {
    
        }
    
        init (callback) {
            // do something async and call the callback:
            callback.bind(this)();
        }
    }
    
  2. Verwenden Sie einen Builder. Ich habe nicht viel davon in Javascript gesehen, aber dies ist eine der häufigsten Umgehungen in Java, wenn ein Objekt asynchron erstellt werden muss. Natürlich wird das Builder-Muster verwendet, wenn ein Objekt erstellt wird, das viele komplizierte Parameter erfordert. Was genau der Anwendungsfall für asynchrone Builder ist. Der Unterschied besteht darin, dass ein asynchroner Builder kein Objekt zurückgibt, sondern ein Versprechen dieses Objekts:

    Verwendungszweck:

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

    Implementierung:

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

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

Hinweis: Obwohl in den obigen Beispielen Versprechungen für den asynchronen Builder verwendet werden, sind diese nicht unbedingt erforderlich. Sie können genauso einfach einen Builder schreiben, der einen Rückruf akzeptiert.


Hinweis zum Aufruf von Funktionen in statischen Funktionen.

Dies hat nichts mit asynchronen Konstruktoren zu tun, sondern mit dem, was das Schlüsselwort this eigentlich bedeutet (was für Leute, die aus Sprachen kommen, die eine automatische Auflösung von Methodennamen ausführen, etwas überraschend ist, dh Sprachen, die das this-Schlüsselwort nicht benötigen ).

Das Schlüsselwort this bezieht sich auf das instanziierte Objekt. Nicht die Klasse. Daher können Sie this normalerweise nicht in statischen Funktionen verwenden, da die statische Funktion nicht an ein Objekt gebunden ist, sondern direkt an die Klasse gebunden ist.

Das heißt im folgenden Code:

class A {
    static foo () {}
}

Du kannst nicht tun:

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

stattdessen musst du es nennen als:

A.foo();

Daher würde der folgende Code zu einem Fehler führen:

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

Um dies zu beheben, können Sie bar entweder als reguläre Funktion oder als statische Methode definieren:

function bar1 () {}

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

    static bar2 () {}
}
104
slebetman

Sie können das auf jeden Fall tun. Grundsätzlich gilt:

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

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

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

um die klasse zu erstellen benutze:

let instance = await new AsyncConstructor();

Hinweis: Wenn Sie super verwenden müssen, können Sie es nicht innerhalb des asynchronen Rückrufs aufrufen. Sie müssen es außerhalb nennen, damit diese Lösung nicht 100% perfekt ist, aber meiner Meinung nach ist es ziemlich idiomatisch und ich benutze es die ganze Zeit in mein Code.

67
Downgoat

Basierend auf Ihren Kommentaren sollten Sie wahrscheinlich das tun, was jedes andere HTMLElement mit dem Laden von Assets tut: Der Konstruktor muss eine Sideloading-Aktion starten und abhängig vom Ergebnis ein Lade- oder Fehlerereignis generieren.

Ja, das bedeutet Versprechen zu verwenden, aber auch "Dinge auf die gleiche Art und Weise wie jedes andere HTML-Element tun", also sind Sie in guter Gesellschaft. Zum Beispiel:

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

dadurch wird eine asynchrone Last des Quellassets ausgelöst, die, wenn sie erfolgreich ist, mit onload endet und, wenn sie fehlerhaft ist, mit onerror endet. Also, machen Sie Ihre eigene Klasse auch so:

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

Und dann lassen Sie die renderLoaded/renderError-Funktionen mit den Ereignisaufrufen und dem Schattendom umgehen:

  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', ...));
  }

Beachten Sie auch, dass ich Ihre id in eine class geändert habe. Wenn Sie nicht einen sonderbaren Code schreiben, der immer nur eine einzelne Instanz Ihres <e-mail>-Elements auf einer Seite zulässt, können Sie keine eindeutige Kennung verwenden und sie dann einer Reihe von Elementen zuordnen.

Da async-Funktionen Versprechungen sind, können Sie in Ihrer Klasse eine statische Funktion erstellen, die eine async-Funktion ausführt, die die Instanz der Klasse zurückgibt:

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

Aufruf mit let yql = await Yql.init() von einer asynchronen Funktion.

1
Vidar

Wenn Sie vermeidenextend können, können Sie Klassen alle zusammen vermeiden und Funktionszusammensetzung als Konstruktoren verwenden. Sie können die Variablen im Gültigkeitsbereich anstelle von Klassenmitgliedern verwenden:

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

und verwenden Sie es einfach als

const a = await buildA(...);

Wenn Sie TypeScript oder flow verwenden, können Sie sogar die Schnittstelle der -Konstruktoren erzwingen

Interface A {
  getData: object;
}

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

benutze asynchrone Methode im Konstrukt ???

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

Ich habe diesen Testfall basierend auf der Antwort von @ Downgoat erstellt.
Es läuft auf NodeJS. Dies ist der Code von Downgoat, bei dem der asynchrone Teil durch einen Aufruf von setTimeout() bereitgestellt wird. 

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

Mein Anwendungsfall sind DAOs für die Serverseite einer Webanwendung.
Wie ich DAOs sehe, sind sie jeweils einem Datensatzformat zugeordnet, in meinem Fall eine MongoDB-Sammlung wie beispielsweise ein Koch.
.__ Eine CooksDAO-Instanz enthält die Daten eines Kochs.
In meinem unruhigen Geist wäre ich in der Lage, das DAO eines Kochs zu instantiieren, das die cookId als Argument liefert, und die Instantiierung würde das Objekt erstellen und es mit den Daten des Kochs füllen.
.__ Die Notwendigkeit, async Zeug in den Konstruktor zu laufen.
Ich wollte schreiben: 

let cook = new cooksDAO( '12345' );  

verfügbare Eigenschaften wie cook.getDisplayName() haben.
Mit dieser Lösung muss ich Folgendes tun: 

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

das ist dem Ideal sehr ähnlich.
Außerdem muss ich dies in einer async-Funktion tun. 

Mein B-Plan bestand darin, die Daten aus dem Konstruktor heraus zu laden, basierend auf dem @slebetman-Vorschlag, eine init-Funktion zu verwenden, und so etwas zu tun: 

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

was nicht gegen die Regeln verstößt. 

0
Juan Lanus

Variation des Builder-Patterns mit 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

Sie können sofort eine anonyme asynchrone Funktion aufrufen, die eine Nachricht zurückgibt, und diese auf die Nachrichtenvariable setzen. Sie können sich sofort aufgerufene Funktionsausdrücke (IEFES) ansehen, falls Sie mit diesem Muster nicht vertraut sind. Dies wird wie ein Zauber funktionieren.

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