Constructor de clase asíncrono / espera


169

Por el momento, estoy tratando de usar async/awaitdentro de una función de constructor de clase. Esto es para que pueda obtener una e-mailetiqueta personalizada para un proyecto Electron en el que estoy trabajando.

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

Por el momento, sin embargo, el proyecto no funciona, con el siguiente error:

Class constructor may not be an async method

¿Hay alguna forma de eludir esto para poder usar async / wait dentro de esto? En lugar de requerir devoluciones de llamada o .then ()?


66
El propósito de un constructor es asignarle un objeto y luego regresar de inmediato. ¿Puedes ser mucho más específico sobre exactamente por qué crees que tu constructor debería ser asíncrono? Porque estamos casi garantizados lidiando con un problema XY aquí.
Mike 'Pomax' Kamermans

44
@ Mike'Pomax'Kamermans Eso es bastante posible. Básicamente, necesito consultar una base de datos para obtener los metadatos necesarios para cargar este elemento. La consulta de la base de datos es una operación asincrónica y, por lo tanto, requiero alguna forma de esperar a que esto se complete antes de construir el elemento. Prefiero no usar devoluciones de llamada, ya que he usado wait / async en el resto del proyecto y me gustaría mantener la continuidad.
Alexander Craggs

@ Mike'Pomax'Kamermans El contexto completo de esto es un cliente de correo electrónico, donde cada elemento HTML se parece <e-mail data-uid="1028"></email>y desde allí se llena de información utilizando el customElements.define()método.
Alexander Craggs

Prácticamente no quieres que un constructor sea asíncrono. Cree un constructor síncrono que devuelva su objeto y luego use un método como .init()hacer cosas asíncronas. Además, dado que es un elemento HTMLE sublcass, es extremadamente probable que el código que usa esta clase no tenga idea de que es una cosa asíncrona, por lo que es probable que tenga que buscar una solución completamente diferente de todos modos.
jfriend00

Respuestas:


263

Esto nunca puede funcionar.

La asyncpalabra clave permite awaitser utilizada en una función marcada como asyncpero también convierte esa función en un generador de promesas. Entonces, una función marcada con asyncdevolverá una promesa. Un constructor, por otro lado, devuelve el objeto que está construyendo. Por lo tanto, tenemos una situación en la que desea devolver un objeto y una promesa: una situación imposible.

Solo puede usar async / wait donde puede usar promesas porque son esencialmente azúcar de sintaxis para promesas. No puede usar promesas en un constructor porque un constructor debe devolver el objeto a construir, no una promesa.

Hay dos patrones de diseño para superar esto, ambos inventados antes de que se cumplieran las promesas.

  1. Uso de una init()función. Esto funciona un poco como jQuery's .ready(). El objeto que cree solo se puede usar dentro de su propia función inito readyfunción:

    Uso:

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

    Implementación:

    class myClass {
        constructor () {
    
        }
    
        init (callback) {
            // do something async and call the callback:
            callback.bind(this)();
        }
    }
    
  2. Usa un constructor. No he visto que esto se use mucho en JavaScript, pero esta es una de las soluciones más comunes en Java cuando un objeto debe construirse de forma asíncrona. Por supuesto, el patrón de construcción se usa al construir un objeto que requiere muchos parámetros complicados. Cuál es exactamente el caso de uso para los constructores asincrónicos. La diferencia es que un generador asíncrono no devuelve un objeto sino una promesa de ese objeto:

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

    Implementación:

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

    Implementación con 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);
        }
    }
    

Nota: aunque en los ejemplos anteriores usamos promesas para el generador asíncrono, no son estrictamente necesarios. Puede escribir fácilmente un generador que acepte una devolución de llamada.


Nota sobre funciones de llamada dentro de funciones estáticas.

Esto no tiene nada que ver con los constructores asíncronos, sino con lo que thisrealmente significa la palabra clave (lo que puede ser un poco sorprendente para las personas que provienen de idiomas que hacen resolución automática de nombres de métodos, es decir, idiomas que no necesitan la thispalabra clave).

La thispalabra clave se refiere al objeto instanciado. No la clase Por lo tanto, normalmente no puede usar thisfunciones estáticas internas ya que la función estática no está vinculada a ningún objeto, sino que está vinculada directamente a la clase.

Es decir, en el siguiente código:

class A {
    static foo () {}
}

Tú no puedes hacer:

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

en su lugar, debe llamarlo como:

A.foo();

Por lo tanto, el siguiente código provocaría un error:

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

Para solucionarlo, puede hacer baruna función regular o un método estático:

function bar1 () {}

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

    static bar2 () {}
}

tenga en cuenta que, según los comentarios, la idea es que este es un elemento html, que generalmente no tiene un manual init()pero tiene la funcionalidad vinculada a algún atributo específico como srco href(y en este caso data-uid) , lo que significa usar un setter que ambos enlaza y arranca el inicio cada vez que se vincula un nuevo valor (y posiblemente también durante la construcción, pero por supuesto sin esperar en la ruta de código resultante)
Mike 'Pomax' Kamermans

Debe comentar por qué la respuesta a continuación es insuficiente (si lo es). O abordarlo de otra manera.
Augie Gardner

Tengo curiosidad por qué bindse requiere en el primer ejemplo callback.bind(this)();? ¿Para que pueda hacer cosas this.otherFunc()dentro de la devolución de llamada?
Alexander Craggs

1
@AlexanderCraggs Es solo conveniencia para que thisen la devolución de llamada se haga referencia myClass. Si siempre usa en myObjlugar de thisno lo necesita
slebetman

1
Actualmente es una limitación en el idioma, pero no veo por qué en el futuro no puede tener const a = await new A()de la misma manera que tenemos funciones regulares y funciones asíncronas.
7ynk3r

138

Usted puede sin duda hacer esto. Básicamente:

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

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

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

para crear el uso de la clase:

let instance = await new AsyncConstructor();

Sin embargo, esta solución tiene algunas caídas cortas:

supernota : si necesita usar super, no puede llamarlo dentro de la devolución de llamada asíncrona.

Nota de TypeScript: esto causa problemas con TypeScript porque el constructor devuelve el tipo en Promise<MyClass>lugar de MyClass. No hay una forma definitiva de resolver esto que yo sepa. Una forma potencial sugerida por @blitter es colocar /** @type {any} */al comienzo del cuerpo del constructor. Sin embargo, no sé si esto funciona en todas las situaciones.


1
@PAStheLoD no creo que va a resolver al objeto sin el regreso sin embargo usted está diciendo que lo hace voy a revisar y actualizar las especificaciones ...
Downgoat

2
@JuanLanus, el bloque asíncrono capturará automáticamente los parámetros, por lo que para el argumento x solo tienes que hacerloconstructor(x) { return (async()=>{await f(x); return this})() }
Downgoat

1
@PAStheLoD: return thises necesario, porque mientras lo constructorhace automáticamente por usted, ese asincrónico IIFE no lo hace, y terminará devolviendo un objeto vacío.
Dan Dascalescu

1
Actualmente a partir de TS 3.5.1 dirigido a ES5, ES2017, ES2018 (y probablemente a otros, pero no lo he comprobado) si realiza una devolución en un constructor, aparece este mensaje de error: "El tipo de retorno de la firma del constructor debe ser asignable a tipo de instancia de la clase ". El tipo de IIFE es una Promesa <esto>, y dado que la clase no es una Promesa <T>, no veo cómo podría funcionar. (¿Qué podría devolver aparte de 'esto'?) Entonces, esto significa que ambos retornos son innecesarios. (El exterior es un poco peor, ya que conduce a un error de compilación.)
PAStheLoD

3
@PAStheLoD sí, eso es una limitación mecanografiada. Por lo general, en JS, una clase Tdebería regresar Tcuando se construye, pero para obtener la capacidad asíncrona que devolvemos, Promise<T>que se resuelve this, pero eso confunde el mecanografiado. Necesita el retorno externo; de lo contrario, no sabrá cuándo termina la promesa; como resultado, este enfoque no funcionará en TypeScript (a menos que haya algún truco con quizás un alias de tipo). Sin embargo, no soy un experto en mecanografía, así que no puedo hablar de eso
Downgoat

7

Debido a que las funciones asíncronas son promesas, puede crear una función estática en su clase que ejecute una función asíncrona que devuelva la instancia de la clase:

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

Llamar con let yql = await Yql.init()desde una función asíncrona.


5

Según sus comentarios, probablemente debería hacer lo que cualquier otro elemento HTMLE con carga de activos hace: hacer que el constructor inicie una acción de carga lateral, generando un evento de carga o error dependiendo del resultado.

Sí, eso significa usar promesas, pero también significa "hacer las cosas de la misma manera que cualquier otro elemento HTML", por lo que estás en buena compañía. Por ejemplo:

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

Esto inicia una carga asincrónica del activo de origen que, cuando tiene éxito, termina onloady cuando sale mal, termina onerror. Entonces, haz que tu propia clase haga esto también:

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

Y luego haces que las funciones renderLoaded / renderError se ocupen de las llamadas de evento y shadow 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', ...));
  }

También tenga en cuenta que cambié su ida a class, porque a menos que escriba un código extraño para permitir solo una sola instancia de su <e-mail>elemento en una página, no puede usar un identificador único y luego asignarlo a un grupo de elementos.


2

Hice este caso de prueba basado en la respuesta de @ Downgoat.
Se ejecuta en NodeJS. Este es el código de Downgoat donde la parte asíncrona es proporcionada por una setTimeout()llamada.

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

Mi caso de uso es DAO para el lado del servidor de una aplicación web.
Como veo DAO, cada uno está asociado a un formato de registro, en mi caso una colección MongoDB como, por ejemplo, un cocinero.
Una instancia de cooksDAO contiene los datos de un cocinero.
En mi mente inquieta, podría crear una instancia del DAO de un cocinero proporcionando el cookId como argumento, y la creación de instancias crearía el objeto y lo poblaría con los datos del cocinero.
De ahí la necesidad de ejecutar cosas asíncronas en el constructor.
Yo queria escribir:

let cook = new cooksDAO( '12345' );  

tener propiedades disponibles como cook.getDisplayName().
Con esta solución tengo que hacer:

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

que es muy similar al ideal
Además, necesito hacer esto dentro de una asyncfunción.

Mi plan B era dejar que los datos se cargaran fuera del constructor, según la sugerencia de @slebetman de usar una función init, y hacer algo como esto:

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

que no rompe las reglas


2

utilizar el método asincrónico en la construcción?

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 solución provisional

Puede crear un async init() {... return this;}método, y luego hacerlo new MyClass().init()siempre que normalmente solo diría new MyClass().

Esto no está limpio porque depende de todos los que usan su código, y de usted mismo, para instanciar siempre el objeto de esa manera. Sin embargo, si solo usa este objeto en un lugar o dos en particular en su código, tal vez podría estar bien.

Sin embargo, se produce un problema importante porque ES no tiene un sistema de tipos, por lo que si olvida llamarlo, acaba de regresar undefinedporque el constructor no devuelve nada. Ups Mucho mejor sería hacer algo como:

Lo mejor que podría hacer sería:

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 solución del método de fábrica (un poco mejor)

Sin embargo, entonces podría hacer accidentalmente un nuevo AsyncOnlyObject, probablemente debería crear una función de fábrica que use Object.create(AsyncOnlyObject.prototype)directamente:

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

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

Sin embargo, digamos que desea usar este patrón en muchos objetos ... podría abstraerlo como decorador o algo que (verbosely, ugh) llama después de definir como postProcess_makeAsyncInit(AsyncOnlyObject), pero aquí lo voy a usar extendsporque encaja en la semántica de la subclase (las subclases son clase padre + extra, ya que deben obedecer el contrato de diseño de la clase padre y pueden hacer cosas adicionales; una subclase asíncrona sería extraña si el padre no fuera también asíncrono, porque no se podría inicializar de la misma manera camino):


Solución resumida (versión extendida / subclase)

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}

(no usar en producción: no he pensado en escenarios complicados como si esta es la forma correcta de escribir un contenedor para argumentos de palabras clave).


2

A diferencia de lo que otros han dicho, puede hacerlo funcionar.

JavaScript classes puede devolver literalmente cualquier cosa de su constructor, incluso una instancia de otra clase. Por lo tanto, puede devolver un Promiseconstructor de su clase que se resuelva en su instancia real.

A continuación se muestra un ejemplo:

export class Foo {

    constructor() {

        return (async () => {

            // await anything you want

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

Luego, creará instancias de Fooesta manera:

const foo = await new Foo();

1

Si puede evitar extend , puede evitar todas las clases juntas y usar la composición de funciones como constructores . Puede usar las variables en el ámbito en lugar de los miembros de la clase:

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

y simple usarlo como

const a = await buildA(...);

Si está utilizando typecript o flow, incluso puede aplicar la interfaz de los constructores

Interface A {
  getData: object;
}

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

0

Variación en el patrón del constructor, 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

Puede invocar inmediatamente una función asincrónica anónima que devuelve el mensaje y establecerlo en la variable del mensaje. Es posible que desee echar un vistazo a las expresiones de función invocadas inmediatamente (IEFES), en caso de que no esté familiarizado con este patrón. Esto funcionará como un encanto.

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

-1

La respuesta aceptada de @ slebetmen explica bien por qué esto no funciona. Además de los dos patrones presentados en esa respuesta, otra opción es acceder solo a sus propiedades asíncronas a través de un captador asíncrono personalizado. El constructor () puede desencadenar la creación asíncrona de las propiedades, pero el captador comprueba si la propiedad está disponible antes de usarla o devolverla.

Este enfoque es particularmente útil cuando desea inicializar un objeto global una vez en el inicio y desea hacerlo dentro de un módulo. En lugar de inicializar en su index.jsy pasar la instancia en los lugares que la necesitan, simplemente requiresu módulo donde sea que se necesite el objeto global.

Uso

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

Implementación

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

A las otras respuestas les falta lo obvio. Simplemente llame a una función asincrónica de su constructor:

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

Como otra respuesta "obvia" aquí , esta no hará lo que el programador comúnmente espera de un constructor, es decir, que el contenido se establece cuando se crea el objeto.
Dan Dascalescu

2
@DanDascalescu Está configurado, de forma asíncrona, que es exactamente lo que requiere el interrogador. Su punto es que el contenido no se establece sincrónicamente cuando se crea el objeto, lo cual no es requerido por la pregunta. Es por eso que la pregunta es sobre el uso de wait / async desde un constructor. He demostrado cómo puede invocar tanto en espera / asíncrono como desee de un constructor llamando a una función asíncrona desde él. He respondido la pregunta perfectamente.
Navigateur

@Navigateur fue la misma solución que se me ocurrió, pero los comentarios sobre otra pregunta similar sugieren que no debería hacerse de esta manera. El principal problema de ser una promesa se pierde en el constructor, y esto es antipatrón. ¿Tiene alguna referencia donde recomiende este enfoque de llamar a una función asíncrona desde su constructor?
Marklar

1
@Marklar sin referencias, ¿por qué necesita alguna? No importa si algo se "pierde" si no lo necesita en primer lugar. Y si necesita la promesa, es trivial agregar this.myPromise =(en el sentido general), por lo que no es un antipatrón en ningún sentido. Hay casos perfectamente válidos para la necesidad de poner en marcha un algoritmo asíncrono, después de la construcción, que no tiene valor de retorno en sí mismo, y agregar uno de nosotros de todos modos, por lo que quien esté aconsejando no hacerlo está malinterpretando algo
Navigateur

1
Gracias por tomarse el tiempo para responder. Estaba buscando lecturas adicionales debido a las respuestas conflictivas aquí en Stackoverflow. Esperaba confirmar algunas de las mejores prácticas para este escenario.
Marklar

-2

Debería agregar una thenfunción a la instancia. Promiselo reconocerá como un objeto compatible con Promise.resolveautomáticamente

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)no funcionará, ya thisque todavía es un thenable. Esto lleva a una resolución recursiva interminable.
Bergi
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.