Im Moment versuche ich, async/await
innerhalb einer Klassenkonstruktorfunktion zu verwenden. Dies ist so, dass ich ein benutzerdefiniertes "E-Mail"-Tag für ein Electron-Projekt erhalten kann, 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>
`
}
})
Im Moment funktioniert das Projekt jedoch nicht, mit folgendem Fehler:
Class constructor may not be an async method
Gibt es eine Möglichkeit, dies zu umgehen, so dass ich async/await in diesem Projekt verwenden kann? Anstatt Callbacks oder .then() zu benötigen?
Das kann nie funktionieren.
Das Schluesselwort async
erlaubt die Verwendung von await
in einer Funktion, die als async
markiert ist, aber es wandelt diese Funktion auch in einen Versprechensgenerator um. Eine mit async
markierte Funktion wird also ein Versprechen zurückgeben. Ein Konstruktor hingegen gibt das Objekt zurück, das er konstruiert. Wir haben also eine Situation, in der Sie sowohl ein Objekt als auch ein Versprechen zurückgeben wollen: eine unmögliche Situation.
Sie können async/await nur dort verwenden, wo Sie Versprechen verwenden können, weil sie im Wesentlichen Syntaxzucker für Versprechen sind. Sie können keine Versprechen in einem Konstruktor verwenden, weil ein Konstruktor das zu konstruierende Objekt zurückgeben muss, nicht ein Versprechen.
Es gibt zwei Entwurfsmuster, um dieses Problem zu lösen, die beide erfunden wurden, bevor es Versprechen gab.
init()
Funktion. Das funktioniert ein bisschen wie jQuery's .ready()
. Das von Ihnen erstellte Objekt kann nur innerhalb seiner eigenen init
oder ready
Funktion verwendet werden:Verwendung:
var myObj = new myClass();
myObj.init(function() {
// hier drin können Sie myObj verwenden
});
Implementierung:
class myClass {
constructor () {
}
init (callback) {
// etwas asynchron tun und den Callback aufrufen:
callback.bind(this)();
}
}
Verwendung:
myClass.build().then(function(myObj) {
// myObj wird durch das Versprechen zurückgegeben,
// nicht durch den Konstruktor
// oder Builder
});
// mit async/await:
async function foo () {
var myObj = await myClass.build();
}
Implementierung:
class myClass {
constructor (async_param) {
if (typeof async_param === 'undefined') {
throw new Error('Kann nicht direkt aufgerufen werden');
}
}
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('Kann nicht direkt aufgerufen werden');
}
}
static async build () {
var async_result = await doSomeAsyncStuff();
return new myClass(async_result);
}
}
Hinweis: Obwohl wir in den obigen Beispielen Versprechen für den asynchronen Builder verwenden, sind sie streng genommen nicht notwendig. Sie können genauso gut einen Builder schreiben, der einen Callback akzeptiert.
Dies hat nichts mit asynchronen Konstruktoren zu tun, sondern damit, was das Schlüsselwort this
tatsächlich bedeutet (was für Leute, die aus Sprachen kommen, die Methodennamen automatisch auflösen, also Sprachen, die das Schlüsselwort this
nicht brauchen, etwas überraschend sein mag).
Das Schlüsselwort this
bezieht sich auf das instanziierte Objekt. Nicht die Klasse. Daher können Sie normalerweise this
nicht innerhalb statischer Funktionen verwenden, da die statische Funktion nicht an ein Objekt, sondern direkt an die Klasse gebunden ist.
Das heißt, im folgenden Code:
class A {
static foo () {}
}
Das können Sie nicht tun:
var a = new A();
a.foo() // NOPE!!
stattdessen müssen Sie es aufrufen 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 den Fehler zu beheben, können Sie bar
entweder zu einer regulären Funktion oder zu einer statischen Methode machen:
function bar1 () {}
class A {
static foo () {
bar1(); // this is OK
A.bar2(); // this is OK
}
static bar2 () {}
}
Basierend auf Ihren Kommentaren sollten Sie wahrscheinlich tun, was jedes andere HTMLElement mit Asset-Loading tut: den Konstruktor eine Sideloading-Aktion starten lassen, die je nach Ergebnis ein Lade- oder Fehlerereignis erzeugt.
Ja, das bedeutet die Verwendung von Versprechen, aber es bedeutet auch, dass Sie die Dinge auf die gleiche 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";
Dies löst ein asynchrones Laden des Quelltextes aus, das, wenn es erfolgreich ist, in onload
endet und wenn es schief geht, in onerror
endet. Lassen Sie also Ihre eigene Klasse dies auch tun:
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 Shadowdom 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, denn wenn Sie nicht irgendeinen seltsamen Code schreiben, um nur eine einzige Instanz Ihres <e-mail>
-Elements auf einer Seite zuzulassen, können Sie keinen eindeutigen Bezeichner verwenden und ihn dann einer Reihe von Elementen zuweisen.
Sie sollten die Funktion then
zur Instanz hinzufügen. Promise" wird es automatisch als ein Thenable-Objekt mit "Promise.resolve" erkennen.
const asyncSymbol = Symbol();
class MyClass {
constructor() {
this.asyncData = null
}
then(resolve, reject) {
return (this[asyncSymbol] = this[asyncSymbol] || new Promise((innerResolve, innerReject) => {
this.asyncData = { a: 1 }
setTimeout(() => innerResolve(this.asyncData), 3000)
})).then(resolve, reject)
}
}
async function wait() {
const asyncData = await new MyClass();
alert('run 3s later')
alert(asyncData.a)
}