2017-04-15 8 views
8

Im Moment versuche ich async/await innerhalb einer Klassenkonstruktorfunktion zu verwenden. Dies ist so, dass ich einen benutzerdefinierten e-mail-Tag für ein Electron-Projekt bekommen kann, an dem ich gerade arbeite. jedochAsync/Await Class Constructor

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 hat das Projekt nicht funktioniert, mit dem folgenden Fehler:

Class constructor may not be an async method 

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

+3

Zweck eines Konstruktors ist es, Ihnen ein Objekt zuzuordnen und dann sofort zurückzukehren. Kannst du viel genauer auf * genau warum * können Sie denken, dass Ihr Konstruktor async sein sollte? Weil wir hier fast garantiert mit einem [XY-Problem] umgehen können (https://meta.stackexchange.com/a/66378). –

+1

@ Mike'Pomax'Kamermans Das ist durchaus möglich. Grundsätzlich muss ich eine Datenbank abfragen, um die Metadaten zum Laden dieses Elements zu erhalten. Das Abfragen der Datenbank ist eine asynchrone Operation, und daher muss ich darauf warten, dass dies abgeschlossen ist, bevor das Element erstellt wird. Ich würde Callbacks lieber nicht verwenden, da ich während des gesamten Rests des Projekts auf async/async gewartet habe und die Kontinuität beibehalten möchte. –

+0

@ Mike'Pomax'Kamermans Der vollständige Kontext ist ein E-Mail-Client, in dem jedes HTML-Element ähnlich aussieht wie 'und von dort mit Informationen gefüllt wird mit den' customElements .define() 'Methode. –

Antwort

25

Dies kann nie arbeiten.

Das Schlüsselwort async ermöglicht die Verwendung von await in einer Funktion, die als async markiert ist, aber es wandelt diese Funktion auch in einen Versprechensgenerator um. Eine Funktion, die mit async markiert ist, gibt also ein Versprechen zurück. 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 nur Async verwenden/erwarten, wo Sie Versprechungen verwenden können, weil sie im Wesentlichen Syntaxzucker für Versprechen sind. Sie können Versprechungen in einem Konstruktor nicht verwenden, da ein Konstruktor das zu erstellende Objekt und kein Versprechen zurückgeben muss.

Es gibt zwei Entwurfsmuster, um dies zu überwinden, beide erfunden, bevor Versprechen gegeben wurden.

  1. Verwendung einer init()-Funktion. Das funktioniert ein bisschen wie jQuerys .ready(). Das Objekt, das Sie erstellen kann nur innerhalb seiner eigenen init oder ready Funktion verwendet werden:

    Verbrauch:

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

    Umsetzung:

    class myClass { 
        constructor() { 
    
        } 
    
        init (callback) { 
         // do something async and call the callback: 
         callback.bind(this)(); 
        } 
    } 
    
  2. einen Builder verwenden. Ich habe nicht gesehen, dass dies viel in Javascript verwendet, aber dies ist einer der häufigsten Workarounds 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. Welches ist genau der Anwendungsfall für asynchrone Builder. Der Unterschied besteht darin, dass ein Asynchron-Builder kein Objekt zurück, sondern ein Versprechen des Objekts:

    Verbrauch:

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

    Umsetzung:

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

    Umsetzung mit Asynchron/erwarten:

    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.

+0

Beachten Sie, dass basierend auf den Kommentaren die Idee ist, dass dies ein HTML-Element ist , die typischerweise kein manuelles 'init()' hat, aber die Funktionalität an ein bestimmtes Attribut gebunden hat, wie 'src' oder' href' (und in diesem Fall 'data-uid'), was bedeutet, dass beide einen Setter verwenden bindet und startet die Init jedes Mal, wenn ein neuer Wert gebunden wird (und möglicherweise auch während der Konstruktion, aber natürlich ohne auf den resultierenden Codepfad zu warten) –

3

Basierend auf Ihren Kommentaren sollten Sie wahrscheinlich tun, was jedes andere HTMLElement mit dem Laden von Assets tut: Lassen Sie den Konstruktor eine Sideloading-Aktion starten, die je nach Ergebnis ein Lade- oder Fehlerereignis generiert.

Ja, das bedeutet Versprechungen zu verwenden, aber es bedeutet auch "Dinge genauso zu machen wie jedes andere HTML-Element", 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"; 

diese weg tritt eine asynchrone Belastung der Quelle Vermögenswert, der, wenn es gelingt, in onload endet und wenn es schief geht, endet in onerror. Also, machen Sie 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 machen Sie die renderLoaded/renderError Funktionen befassen sich mit dem Fall, Anrufe und Schatten 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', ...)); 
    } 

Beachten Sie auch, änderte ich Ihre id zu einem class, denn wenn Sie nicht irgendeinen merkwürdigen Code schreiben, der nur eine einzige Instanz Ihres <e-mail> Elements auf einer Seite erlaubt, können Sie keinen eindeutigen Bezeichner verwenden und ihn dann einer Reihe von Elementen zuweisen.

1

Sie können auch eine selbstausführende async anonyme Funktion in Ihrem Konstruktor erstellen:

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(); 
+2

Dies funktioniert nicht wie erwartet. Versuchen Sie anstelle von 'new MyClass()' 'const obj = new MyClass(); console.log (obj.someProperty) '. Sie erhalten "undefiniert", weil "someProperty" erst nach dem Erstellen des Objekts "bar" 2000 ms * zugewiesen wird. Die Erwartung ist, dass, sobald ich ein Objekt erstellt habe, "someProperty" bereits eingerichtet ist. Dieser Code veranschaulicht, warum Konstruktoren * nicht async sein sollten. –

+1

Ich stimme zu. Obwohl ich denke, dass diese Lösung für einige Anwendungsfälle vollkommen in Ordnung ist, würde ich sie nicht empfehlen. Wenn Sie in Ihrem Konstruktor etwas Asynchrones ausführen müssen, könnte das Design Ihrer Anwendung sowieso besser sein. – afterburn

-3

Sie sollten then Funktion Instanz hinzufügen. Promise wird es als thenable Objekt erkennen mit Promise.resolve automatisch

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') 
} 
+0

'innerResolve (this)' wird nicht funktionieren, da 'this' noch immer thenable ist. Dies führt zu einer unendlichen rekursiven Auflösung. – Bergi

+0

Sorry, mein Fehler hat die Rückgabe der 'then' Funktion nicht überschrieben, um Rekursion zu vermeiden. –

-2

Die anderen Antworten, die offensichtlich fehlen. Rufen Sie einfach eine asynchrone Funktion von Ihrem Konstruktor auf:

constructor() { 
    setContentAsync(); 
} 

async setContentAsync() { 
    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> 
    ` 
} 
+0

Wie [eine andere" offensichtliche "Antwort hier] (https://stackoverflow.com/a/47611907/1269037), wird dies nicht tun was der Programmierer normalerweise von einem Konstruktor erwartet, dh dass der Inhalt beim Erstellen des Objekts gesetzt wird. –

+0

@DanDascalescu Es wird nur asynchron gesetzt, und genau das verlangt der Fragesteller. Ihr Punkt ist, dass der Inhalt beim Erstellen des Objekts nicht synchron gesetzt wird, was für die Frage nicht erforderlich ist. Deshalb geht es bei der Frage darum, in einem Konstruktor "wait/async" zu verwenden. Ich habe demonstriert, wie Sie von einem Konstruktor so viel wie erwartet/async aufrufen können, indem Sie eine asynchrone Funktion aufrufen. Ich habe die Frage perfekt beantwortet. – Navigateur