Costruttore di classe asincrono / in attesa


169

Al momento, sto tentando di utilizzare async/awaituna funzione di costruzione di classi. Questo è così che posso ottenere un e-mailtag personalizzato per un progetto Electron a cui sto lavorando.

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

Al momento, tuttavia, il progetto non funziona, con il seguente errore:

Class constructor may not be an async method

C'è un modo per aggirare questo in modo che io possa usare async / wait all'interno di questo? Invece di richiedere callback o .then ()?


6
Lo scopo di un costruttore è quello di allocare un oggetto e restituirlo immediatamente. Puoi essere molto più specifico su esattamente perché pensi che il tuo costruttore dovrebbe essere asincrono? Perché qui siamo quasi certi di affrontare un problema XY .
Mike 'Pomax' Kamermans,

4
@ Mike'Pomax'Kamermans Questo è abbastanza possibile. Fondamentalmente, ho bisogno di interrogare un database per ottenere i metadati richiesti per caricare questo elemento. L'interrogazione del database è un'operazione asincrona e quindi ho bisogno di un modo di attendere che questo venga completato prima di costruire l'elemento. Preferirei non usare i callback, poiché ho usato waitit / async per tutto il resto del progetto e vorrei mantenere la continuità.
Alexander Craggs,

@ Mike'Pomax'Kamermans Il contesto completo di questo è un client di posta elettronica, in cui ogni elemento HTML appare simile <e-mail data-uid="1028"></email>e da lì è popolato con informazioni usando il customElements.define()metodo.
Alexander Craggs,

Praticamente non vuoi che un costruttore sia asincrono. Crea un costruttore sincrono che restituisca il tuo oggetto e poi usa un metodo come .init()fare cose asincrone. Inoltre, dal momento che sei sottoclasse HTMLElement, è estremamente probabile che il codice che utilizza questa classe non abbia idea che sia una cosa asincrona, quindi probabilmente dovrai cercare una soluzione completamente diversa.
jfriend00,

Risposte:


264

Questo non può mai funzionare.

La asyncparola chiave consente awaitdi essere utilizzata in una funzione contrassegnata come asyncma converte anche quella funzione in un generatore di promesse. Quindi una funzione contrassegnata con asyncrestituirà una promessa. Un costruttore invece restituisce l'oggetto che sta costruendo. Quindi abbiamo una situazione in cui vuoi sia restituire un oggetto che una promessa: una situazione impossibile.

Puoi usare asincronizzazione / wait solo dove puoi usare le promesse perché sono essenzialmente zucchero di sintassi per le promesse. Non puoi usare le promesse in un costruttore perché un costruttore deve restituire l'oggetto da costruire, non una promessa.

Ci sono due modelli di progettazione per ovviare a questo, entrambi inventati prima che le promesse fossero in circolazione.

  1. Uso di una init()funzione. Funziona un po 'come quello di jQuery .ready(). L'oggetto che crei può essere utilizzato solo al suo interno inito alla sua readyfunzione:

    Uso:

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

    Implementazione:

    class myClass {
        constructor () {
    
        }
    
        init (callback) {
            // do something async and call the callback:
            callback.bind(this)();
        }
    }
  2. Usa un costruttore. Non l'ho visto molto usato in JavaScript ma questo è uno dei metodi più comuni per aggirare Java quando un oggetto deve essere costruito in modo asincrono. Ovviamente, il modello builder viene utilizzato quando si costruisce un oggetto che richiede molti parametri complicati. Che è esattamente il caso d'uso per i costruttori asincroni. La differenza è che un builder asincrono non restituisce un oggetto ma una promessa di quell'oggetto:

    Uso:

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

    Implementazione:

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

    Implementazione con asincrono / 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);
        }
    }

Nota: sebbene negli esempi precedenti usiamo promesse per il costruttore asincrono, non sono strettamente necessari. Puoi anche scrivere facilmente un builder che accetta un callback.


Nota sulle funzioni di chiamata all'interno delle funzioni statiche.

Questo non ha nulla a che fare con i costruttori asincroni, ma con ciò thische significa effettivamente la parola chiave (il che potrebbe essere un po 'sorprendente per le persone che provengono da lingue che fanno auto-risoluzione dei nomi dei metodi, cioè lingue che non hanno bisogno della thisparola chiave).

La thisparola chiave si riferisce all'oggetto istanziato. Non la classe. Pertanto non è possibile utilizzare normalmente thisfunzioni statiche interne poiché la funzione statica non è associata a nessun oggetto ma è direttamente associata alla classe.

Vale a dire, nel seguente codice:

class A {
    static foo () {}
}

Non puoi fare:

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

invece devi chiamarlo come:

A.foo();

Pertanto, il codice seguente genererebbe un errore:

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

Per risolverlo puoi fare baruna funzione normale o un metodo statico:

function bar1 () {}

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

    static bar2 () {}
}

si noti che sulla base dei commenti, l'idea è che si tratta di un elemento html, che in genere non ha un manuale init()ma ha la funzionalità legata ad alcuni attributi specifici come srco href(e in questo caso data-uid) , il che significa utilizzare un setter che entrambi vincola e dà il via all'init ogni volta che viene associato un nuovo valore (e possibilmente anche durante la costruzione, ma ovviamente senza attendere il percorso del codice risultante)
Mike 'Pomax' Kamermans

Dovresti commentare perché la risposta di seguito è insufficiente (se lo è). O affrontalo diversamente.
Augie Gardner,

Sono curioso di sapere perché bindè richiesto nel primo esempio callback.bind(this)();? In modo che tu possa fare cose come this.otherFunc()nel callback?
Alexander Craggs,

1
@AlexanderCraggs È solo praticità in modo che thisnel callback si riferisca myClass. Se usi sempre al myObjposto thistuo non ne hai bisogno
slebetman

1
Attualmente è un limite al linguaggio ma non vedo perché in futuro non puoi avere const a = await new A()allo stesso modo abbiamo funzioni regolari e funzioni asincrone.
7ynk3r,

138

È possibile sicuramente fare questo. Fondamentalmente:

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

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

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

per creare la classe usa:

let instance = await new AsyncConstructor();

Questa soluzione presenta tuttavia alcune brevi cadute:

supernota : se è necessario utilizzare super, non è possibile chiamarlo nel callback asincrono.

Nota di TypeScript: ciò causa problemi con TypeScript perché il costruttore restituisce type Promise<MyClass>anziché MyClass. Non esiste un modo definitivo per risolvere ciò che conosco. Un potenziale modo suggerito da @blitter è quello di mettere /** @type {any} */all'inizio del corpo del costruttore - non so se questo funzioni in tutte le situazioni.


1
@PAStheLoD Non credo che risolverà l'oggetto senza il ritorno, tuttavia stai dicendo che lo farà,
rivedrò le

2
@JuanLanus il blocco asincrono acquisirà automaticamente i parametri, quindi per l'argomento x devi solo farloconstructor(x) { return (async()=>{await f(x); return this})() }
Downgoat

1
@PAStheLoD: return thisè necessario, perché mentre lo constructorfa automaticamente per te, quel IIFE asincrono no, e finirai per restituire un oggetto vuoto.
Dan Dascalescu,

1
Attualmente a partire da TS 3.5.1 indirizzato a ES5, ES2017, ES2018 (e probabilmente altri, ma non ho verificato) se si esegue un ritorno in un costruttore viene visualizzato questo messaggio di errore: "Il tipo di ritorno della firma del costruttore deve essere assegnabile al tipo di istanza della classe ". Il tipo di IIFE è una Promessa <questo>, e poiché la classe non è una Promessa <T>, non vedo come potrebbe funzionare. (Cosa potresti restituire se non "questo"?) Quindi questo significa che entrambi i resi non sono necessari. (Quello esterno è un po 'peggio, dal momento che porta a un errore di compilazione.)
PAStheLoD

3
@PAStheLoD sì, questa è una limitazione dattiloscritta. Tipicamente in JS una classe Tdovrebbe tornare Tquando costruita, ma per ottenere l'abilità asincrona ritorniamo Promise<T>che si risolve this, ma che confonde dattiloscritto. Hai bisogno del ritorno esterno, altrimenti non saprai quando il completamento della promessa sarà terminato, di conseguenza questo approccio non funzionerà su TypeScript (a meno che non ci sia qualche hack con forse l'aliasing del tipo?). Non è un esperto di dattiloscritti, quindi non posso parlarne
Downgoat,

7

Poiché le funzioni asincrone sono promesse, è possibile creare una funzione statica sulla propria classe che esegue una funzione asincrona che restituisce l'istanza della 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()

Chiama con let yql = await Yql.init()da una funzione asincrona.


5

Sulla base dei tuoi commenti, dovresti probabilmente fare ciò che fanno tutti gli altri HTMLElement con il caricamento delle risorse: fai in modo che il costruttore inizi un'azione di sideload, generando un evento di carico o errore a seconda del risultato.

Sì, questo significa usare le promesse, ma significa anche "fare le cose allo stesso modo di ogni altro elemento HTML", quindi sei in buona compagnia. Per esempio:

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

questo avvia un carico asincrono dell'asset di origine che, quando ha successo, finisce onloade quando va storto finisce onerror. Quindi, fai in modo che anche la tua classe faccia questo:

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

E poi fai in modo che le funzioni renderLoaded / renderError gestiscano le chiamate agli eventi e l'ombra 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', ...));
  }

Nota anche che ho cambiato il tuo idin a class, perché a meno che tu non scriva un codice strano per consentire una sola istanza del tuo <e-mail>elemento su una pagina, non puoi usare un identificatore univoco e quindi assegnarlo a un gruppo di elementi.


2

Ho realizzato questo test sulla base della risposta di @ Downgoat.
Funziona su NodeJS. Questo è il codice di Downgoat in cui la parte asincrona viene fornita da una setTimeout()chiamata.

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

Il mio caso d'uso è DAO per il lato server di un'applicazione web.
Come vedo i DAO, sono tutti associati a un formato record, nel mio caso una collezione MongoDB come ad esempio un cuoco.
Un'istanza di cooksDAO contiene i dati di un cuoco.
Nella mia mente inquieta sarei in grado di creare un'istanza del DAO di un cuoco fornendo l'argomento Cook come argomento e l'istanza creerebbe l'oggetto e lo popolerebbe con i dati del cuoco.
Quindi la necessità di eseguire roba asincrona nel costruttore.
Volevo scrivere:

let cook = new cooksDAO( '12345' );  

avere proprietà disponibili come cook.getDisplayName().
Con questa soluzione devo fare:

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

che è molto simile all'ideale.
Inoltre, devo farlo all'interno di una asyncfunzione.

Il mio piano B consisteva nel lasciare i dati caricati dal costruttore, in base al suggerimento di @slebetman di usare una funzione init, e fare qualcosa del genere:

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

che non infrange le regole.


2

usa il metodo asincrono nel costrutto ???

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

2

La soluzione di stopgap

Puoi creare un async init() {... return this;}metodo, quindi invece new MyClass().init()ogni volta che diresti normalmente new MyClass().

Questo non è pulito perché si basa su chiunque usi il tuo codice e te stesso per creare un'istanza dell'oggetto in questo modo. Tuttavia, se stai utilizzando questo oggetto solo in un determinato punto o due nel tuo codice, potrebbe andare bene.

Tuttavia, si verifica un problema significativo perché ES non ha un sistema di tipi, quindi se si dimentica di chiamarlo, si è appena tornati undefinedperché il costruttore non restituisce nulla. Ops. Molto meglio sarebbe fare qualcosa del tipo:

La cosa migliore da fare sarebbe:

class AsyncOnlyObject {
    constructor() {
    }
    async init() {
        this.someField = await this.calculateStuff();
    }

    async calculateStuff() {
        return 5;
    }
}

async function newAsync_AsyncOnlyObject() {
    return await new AsyncOnlyObject().init();
}

newAsync_AsyncOnlyObject().then(console.log);
// output: AsyncOnlyObject {someField: 5}

La soluzione del metodo di fabbrica (leggermente migliore)

Tuttavia, potresti accidentalmente fare un nuovo AsyncOnlyObject, probabilmente dovresti semplicemente creare una funzione di fabbrica che utilizza Object.create(AsyncOnlyObject.prototype)direttamente:

async function newAsync_AsyncOnlyObject() {
    return await Object.create(AsyncOnlyObject.prototype).init();
}

newAsync_AsyncOnlyObject().then(console.log);
// output: AsyncOnlyObject {someField: 5}

Comunque dì che vuoi usare questo modello su molti oggetti ... potresti astrarlo come un decoratore o qualcosa che tu (verbosamente, ugh) chiami dopo aver definito come postProcess_makeAsyncInit(AsyncOnlyObject), ma qui userò extendsperché si adatta alla semantica della sottoclasse (le sottoclassi sono la classe genitore + extra, in quanto dovrebbero obbedire al contratto di progettazione della classe genitore e possono fare cose aggiuntive; una sottoclasse asincrona sarebbe strana se anche il genitore non fosse asincrono, perché non potrebbe essere inizializzato lo stesso modo):


Soluzione astratta (versione estesa / sottoclasse)

class AsyncObject {
    constructor() {
        throw new Error('classes descended from AsyncObject must be initialized as (await) TheClassName.anew(), rather than new TheClassName()');
    }

    static async anew(...args) {
        var R = Object.create(this.prototype);
        R.init(...args);
        return R;
    }
}

class MyObject extends AsyncObject {
    async init(x, y=5) {
        this.x = x;
        this.y = y;
        // bonus: we need not return 'this'
    }
}

MyObject.anew('x').then(console.log);
// output: MyObject {x: "x", y: 5}

(non usare in produzione: non ho pensato a scenari complicati come se questo è il modo corretto di scrivere un wrapper per argomenti di parole chiave.)


2

A differenza di altri hanno detto, puoi farlo funzionare.

I JavaScript classpossono restituire letteralmente qualsiasi cosa dalla loro constructor, anche un'istanza di un'altra classe. Quindi, potresti restituire a Promisedal costruttore della tua classe che si risolve nella sua istanza effettiva.

Di seguito è riportato un esempio:

export class Foo {

    constructor() {

        return (async () => {

            // await anything you want

            return this; // Return the newly-created instance
        }).call(this);
    }
}

Quindi, creerai istanze in Fooquesto modo:

const foo = await new Foo();

1

Se puoi evitare extend , puoi evitare le classi tutte insieme e usare la composizione delle funzioni come costruttori . È possibile utilizzare le variabili nell'ambito invece dei membri della classe:

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

e semplicemente usalo come

const a = await buildA(...);

Se stai usando il dattiloscritto o il flusso, puoi persino applicare l'interfaccia dei costruttori

Interface A {
  getData: object;
}

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

0

Variazione sul modello del builder, usando 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

È possibile richiamare immediatamente una funzione asincrona anonima che restituisce il messaggio e impostarlo sulla variabile del messaggio. Si consiglia di dare un'occhiata alle espressioni di funzione (IEFES) immediatamente invocate, nel caso in cui non si abbia familiarità con questo modello. Funzionerà come un fascino.

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

-1

La risposta accettata di @ slebetmen spiega bene perché non funziona. Oltre ai due modelli presentati in quella risposta, un'altra opzione è di accedere alle proprietà asincrone solo tramite un getter asincrono personalizzato. Il costruttore () può quindi attivare la creazione asincrona delle proprietà, ma il getter verifica quindi se la proprietà è disponibile prima di utilizzarla o restituirla.

Questo approccio è particolarmente utile quando si desidera inizializzare un oggetto globale una volta all'avvio e si desidera farlo all'interno di un modulo. Invece di inizializzare nel tuo index.jse passare l'istanza nei luoghi che ne hanno bisogno, semplicemente il requiretuo modulo ovunque sia necessario l'oggetto globale.

uso

const instance = new MyClass();
const prop = await instance.getMyProperty();

Implementazione

class MyClass {
  constructor() {
    this.myProperty = null;
    this.myPropertyPromise = this.downloadAsyncStuff();
  }
  async downloadAsyncStuff() {
    // await yourAsyncCall();
    this.myProperty = 'async property'; // this would instead by your async call
    return this.myProperty;
  }
  getMyProperty() {
    if (this.myProperty) {
      return this.myProperty;
    } else {
      return this.myPropertyPromise;
    }
  }
}

-2

Alle altre risposte manca l'ovvio. Chiama semplicemente una funzione asincrona dal tuo costruttore:

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

Come un'altra risposta "ovvia" qui , questa non farà ciò che il programmatore si aspetta comunemente da un costruttore, ovvero che il contenuto sia impostato al momento della creazione dell'oggetto.
Dan Dascalescu il

2
@DanDascalescu È impostato, in modo asincrono, esattamente ciò che richiede l'interrogante. Il punto è che il contenuto non è impostato in modo sincrono quando viene creato l'oggetto, il che non è richiesto dalla domanda. Ecco perché la domanda riguarda l'uso di waitit / async all'interno di un costruttore. Ho dimostrato come puoi invocare tutta l'attesa / asincrono che desideri da un costruttore chiamando una funzione asincrona da esso. Ho risposto perfettamente alla domanda.
Navigatore

@Navigateur è stata la stessa soluzione che mi è venuta in mente, ma i commenti su un'altra domanda simile suggeriscono che non dovrebbe essere fatto in questo modo. Il problema principale è che una promessa è persa nel costruttore, e questo è antipattern. Hai dei riferimenti in cui raccomanda questo approccio di chiamare una funzione asincrona dal tuo costruttore?
Marklar,

1
@Marklar nessun riferimento, perché ne hai bisogno? Non importa se qualcosa viene "perso" se non ne hai bisogno in primo luogo. E se hai bisogno della promessa, è banale aggiungerethis.myPromise = (in senso generale) quindi non un anti-pattern in alcun senso. Ci sono casi perfettamente validi per la necessità di dare il via a un algoritmo asincrono, al momento della costruzione, che non ha alcun valore di ritorno in sé, e aggiungerne uno semplice in ogni caso, quindi chiunque stia avvisando di non farlo è fraintendimento qualcosa
Navigateur

1
Grazie per il tempo dedicato a rispondere. Stavo cercando ulteriori letture a causa delle risposte contrastanti qui su StackOverflow. Speravo di confermare alcune delle migliori pratiche per questo scenario.
Marklar,

-2

È necessario aggiungere la thenfunzione all'istanza. Promisericonosceranno come un oggetto thenable con Promise.resolveautomaticamente

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

innerResolve(this)non funzionerà, poiché thisè ancora accettabile. Questo porta ad una risoluzione ricorsiva senza fine.
Bergi,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.