Construtor de classe assíncrona / em espera

169

No momento, estou tentando usar async/awaitdentro de uma função de construtor de classe. Isso é para que eu possa obter uma e-mailtag personalizada para um projeto da Electron em que estou trabalhando.

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

No momento, no entanto, o projeto não funciona, com o seguinte erro:

Class constructor may not be an async method

Existe uma maneira de contornar isso para que eu possa usar assíncrono / aguardar dentro disso? Em vez de exigir retornos de chamada ou .then ()?

Alexander Craggs
fonte
6
O objetivo de um construtor é alocar um objeto para você e retornar imediatamente. Você pode ser muito mais específico sobre exatamente por que você acha que seu construtor deve ser assíncrono? Porque estamos quase garantidos lidando com um problema XY aqui.
Mike 'Pomax' Kamermans
4
@ Mike'Pomax'Kamermans Isso é bem possível. Basicamente, preciso consultar um banco de dados para obter os metadados necessários para carregar este elemento. Consultar o banco de dados é uma operação assíncrona e, portanto, eu preciso de alguma maneira de esperar que isso seja concluído antes de construir o elemento. Prefiro não usar retornos de chamada, pois usei wait / async durante o resto do projeto e gostaria de manter a continuidade.
precisa
@ Mike'Pomax'Kamermans O contexto completo disso é um cliente de e-mail, onde cada elemento HTML é semelhante <e-mail data-uid="1028"></email>e de lá é preenchido com informações usando o customElements.define()método.
precisa
Você praticamente não quer que um construtor seja assíncrono. Crie um construtor síncrono que retorne seu objeto e use um método como .init()fazer as coisas assíncronas. Além disso, como você sublinha HTMLElement, é extremamente provável que o código que está usando essa classe não faça ideia de que é uma coisa assíncrona; portanto, é provável que você precise procurar uma solução totalmente diferente.
precisa saber é o seguinte

Respostas:

262

Isso nunca pode funcionar.

A asyncpalavra-chave permite awaitser usada em uma função marcada como asyncmas também converte essa função em um gerador de promessa. Portanto, uma função marcada com asyncretornará uma promessa. Um construtor, por outro lado, retorna o objeto que está construindo. Portanto, temos uma situação em que você deseja retornar um objeto e uma promessa: uma situação impossível.

Você só pode usar async / waitit onde você pode usar promessas, porque elas são essencialmente sintaxe para promessas. Você não pode usar promessas em um construtor porque um construtor deve retornar o objeto a ser construído, não uma promessa.

Existem dois padrões de design para superar isso, ambos inventados antes das promessas.

  1. Uso de uma init()função. Isso funciona um pouco como o jQuery .ready(). O objeto que você cria pode ser usado apenas dentro de sua própria função initou readyfunção:

    Uso:

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

    Implementação:

    class myClass {
        constructor () {
    
        }
    
        init (callback) {
            // do something async and call the callback:
            callback.bind(this)();
        }
    }
  2. Use um construtor. Não vi isso muito usado em javascript, mas essa é uma das soluções mais comuns em Java quando um objeto precisa ser construído de forma assíncrona. Obviamente, o padrão do construtor é usado ao construir um objeto que requer muitos parâmetros complicados. Qual é exatamente o caso de uso para construtores assíncronos. A diferença é que um construtor assíncrono não retorna um objeto, mas uma promessa desse 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();
    }

    Implementação:

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

    Implementação com async / waitit:

    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: embora nos exemplos acima usemos promessas para o construtor assíncrono, elas não são estritamente necessárias. Você pode facilmente escrever um construtor que aceite um retorno de chamada.


Nota sobre como chamar funções dentro de funções estáticas.

Isso não tem nada a ver com construtores assíncronos, mas com o que a palavra-chave thisrealmente significa (o que pode ser um pouco surpreendente para pessoas provenientes de linguagens que resolvem automaticamente nomes de métodos, ou seja, linguagens que não precisam da thispalavra - chave).

A thispalavra-chave refere-se ao objeto instanciado. Não é da turma. Portanto, você normalmente não pode usar thisfunções estáticas internas, pois a função estática não está vinculada a nenhum objeto, mas diretamente à classe.

Ou seja, no seguinte código:

class A {
    static foo () {}
}

Você não pode fazer:

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

em vez disso, você precisa chamá-lo como:

A.foo();

Portanto, o código a seguir resultaria em um erro:

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

Para corrigi-lo, você pode criar baruma função regular ou um método estático:

function bar1 () {}

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

    static bar2 () {}
}
slebetman
fonte
observe que, com base nos comentários, a idéia é que este seja um elemento html, que normalmente não possui um manual, init()mas tem a funcionalidade vinculada a algum atributo específico como srcor href(e neste caso data-uid) , o que significa usar um setter que ambos se liga e arranca init cada vez que um novo valor é obrigado (e, possivelmente, durante a construção, também, mas é claro sem esperar no caminho código resultante)
Mike 'Pomax' Kamermans
Você deve comentar por que a resposta abaixo é insuficiente (se for). Ou resolva o contrário.
Augie Gardner
Estou curioso por que bindé necessário no primeiro exemplo callback.bind(this)();? Para que você possa fazer coisas como this.otherFunc()no retorno de chamada?
Alexander Craggs
1
@AlexanderCraggs É apenas conveniência para que thiso retorno de chamada se refira myClass. Se você sempre usa myObjem vez de thisvocê não precisa dele
slebetman
1
Atualmente, há uma limitação no idioma, mas não vejo por que, no futuro, você não poderá ter const a = await new A()da mesma maneira que temos funções regulares e funções assíncronas.
7ynk3r
138

Você pode definitivamente fazer isso. Basicamente:

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

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

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

para criar a classe use:

let instance = await new AsyncConstructor();

Esta solução tem algumas quedas curtas:

superNota : Se você precisar usar super, não poderá chamá-lo no retorno de chamada assíncrono.

Nota do TypeScript: isso causa problemas com o TypeScript porque o construtor retorna o tipo em Promise<MyClass>vez de MyClass. Não há maneira definitiva de resolver isso que eu saiba. Uma maneira potencial sugerida pelo @blitter é colocar /** @type {any} */no início do corpo do construtor - não sei se isso funciona em todas as situações.

Downgoat
fonte
1
@PAStheLoD Eu não acho que vai resolver para o objeto sem o retorno, contudo, você está dizendo que ele faz isso vou rever especificação e atualização ...
Downgoat
2
@JuanLanus o bloco assíncrono irá capturar automaticamente os parâmetros de modo para o argumento x Você só precisa fazerconstructor(x) { return (async()=>{await f(x); return this})() }
Downgoat
1
@PAStheLoD: return thisé necessário, porque enquanto o constructorfaz automaticamente para você, o assíncrono IIFE não, e você acabará retornando um objeto vazio.
Dan Dascalescu
1
Atualmente, no TS 3.5.1, visando o ES5, ES2017, ES2018 (e provavelmente outros, mas ainda não o verifiquei), se você retornar um construtor, receberá esta mensagem de erro: "O tipo de retorno da assinatura do construtor deve ser atribuído ao tipo de instância da classe ". O tipo do IIFE é uma promessa <este>, e como a classe não é uma promessa <T>, não vejo como isso poderia funcionar. (O que você poderia retornar além de 'this'?) Portanto, isso significa que os dois retornos são desnecessários. (O externo é um pouco pior, pois leva a um erro de compilação.) #
PAStheLoD 30/06/19
3
@PAStheLoD sim, essa é uma limitação de texto datilografado. Normalmente, em JS, uma classe Tdeve retornar Tquando construída, mas para obter a capacidade assíncrona que retornamos, Promise<T>resolvida this, mas isso confunde o texto datilografado. Você precisa do retorno externo, caso contrário não saberá quando a promessa terminar de concluir - como resultado, essa abordagem não funcionará no TypeScript (a menos que haja algum hack com talvez o alias do tipo?). Não é um especialista typescript embora por isso não posso falar sobre isso
Downgoat
7

Como as funções assíncronas são promessas, você pode criar uma função estática em sua classe que execute uma função assíncrona que retorne a instância da 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()

Ligue com let yql = await Yql.init()uma função assíncrona.

Vidar
fonte
5

Com base nos seus comentários, você provavelmente deve fazer o que todos os outros HTMLElement com carregamento de ativos fazem: fazer com que o construtor inicie uma ação de carregamento lateral, gerando um evento de carga ou erro, dependendo do resultado.

Sim, isso significa usar promessas, mas também significa "fazer as coisas da mesma maneira que qualquer outro elemento HTML", para que você esteja em boa companhia. Por exemplo:

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

isso inicia uma carga assíncrona do ativo de origem que, quando é bem-sucedida, termina onloade quando dá errado, termina onerror. Portanto, faça sua própria classe fazer isso também:

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 então você faz as funções renderLoaded / renderError lidar com as chamadas de evento e o 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', ...));
  }

Observe também que eu mudei seu idpara a class, porque, a menos que você escreva algum código estranho para permitir apenas uma única instância do seu <e-mail>elemento em uma página, não será possível usar um identificador exclusivo e atribuí-lo a vários elementos.

Mike 'Pomax' Kamermans
fonte
2

Fiz esse caso de teste com base na resposta de @ Downgoat.
É executado no NodeJS. Este é o código do Downgoat onde a parte assíncrona é fornecida por uma setTimeout()chamada.

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

Meu caso de uso é DAOs para o lado do servidor de um aplicativo Web.
Pelo que vejo DAOs, cada um deles está associado a um formato de registro, no meu caso, uma coleção do MongoDB como, por exemplo, um cozinheiro.
Uma instância cooksDAO mantém os dados de um cozinheiro.
Em minha mente inquieta, eu seria capaz de instanciar o DAO de um cozinheiro, fornecendo o cookId como argumento, e a instanciação criaria o objeto e o preencheria com os dados do cozinheiro.
Daí a necessidade de executar coisas assíncronas no construtor.
Eu queria escrever:

let cook = new cooksDAO( '12345' );  

ter propriedades disponíveis como cook.getDisplayName().
Com esta solução, tenho que fazer:

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

que é muito parecido com o ideal.
Além disso, eu preciso fazer isso dentro de uma asyncfunção.

Meu plano B era deixar os dados carregados fora do construtor, com base na sugestão de @slebetman para usar uma função init, e fazer algo assim:

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

o que não quebra as regras.

Juan Lanus
fonte
2

usar método assíncrono na construção ???

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());
    });
}
Aliaksandr Shpak
fonte
2

A solução paliativa

Você pode criar um async init() {... return this;}método e, em vez disso, new MyClass().init()sempre que você normalmente diznew MyClass() .

Isso não é limpo, pois depende de todos que usam seu código e a você mesmo para sempre instanciar o objeto dessa maneira. No entanto, se você estiver usando apenas esse objeto em um ou dois lugares específicos no seu código, talvez esteja correto.

Um problema significativo ocorre porque o ES não possui um sistema de tipos. Portanto, se você esquecer de chamá-lo, retornará undefined porque o construtor não retornará nada. Opa Muito melhor seria fazer algo como:

A melhor coisa a fazer seria:

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}

A solução do método de fábrica (um pouco melhor)

No entanto, você pode acidentalmente executar o novo AsyncOnlyObject; provavelmente, você deve apenas criar uma função de fábrica que use Object.create(AsyncOnlyObject.prototype)diretamente:

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

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

No entanto, digamos que você queira usar esse padrão em muitos objetos ... você pode abstraí-lo como um decorador ou algo que você chamará depois de definir como postProcess_makeAsyncInit(AsyncOnlyObject), mas aqui eu vou usar extendsporque ele se encaixa na semântica da subclasse (as subclasses são classe pai + extra, pois devem obedecer ao contrato de design da classe pai e podem fazer coisas adicionais; uma subclasse assíncrona seria estranha se o pai também não fosse assíncrono, porque não poderia ser inicializado da mesma forma maneira):


Solução abstraída (versão estendida / subclasse)

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}

(não use na produção: não pensei em cenários complicados, como se essa é a maneira correta de escrever um wrapper para argumentos de palavras-chave.)

ninjagecko
fonte
2

Ao contrário de outros, você pode fazê-lo funcionar.

JavaScript classes pode retornar literalmente qualquer coisa deles constructor, mesmo uma instância de outra classe. Portanto, você pode retornar um Promisedo construtor de sua classe que resolve para sua instância real.

Abaixo está um exemplo:

export class Foo {

    constructor() {

        return (async () => {

            // await anything you want

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

Em seguida, você criará instâncias Foodessa maneira:

const foo = await new Foo();
Davide Cannizzo
fonte
1

Se você puder evitar extend , evite as classes juntas e use a composição de funções como construtores . Você pode usar as variáveis ​​no escopo em vez dos membros da classe:

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

e simples usá-lo como

const a = await buildA(...);

Se você estiver usando texto datilografado ou fluxo, poderá aplicar a interface dos construtores

Interface A {
  getData: object;
}

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

Variação no padrão do construtor, 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;
}
Jeff Lowery
fonte
0

Você pode chamar imediatamente uma função assíncrona anônima que retorne uma mensagem e defina-a como a variável de mensagem. Você pode dar uma olhada nas expressões de função invocadas imediatamente (IEFES), caso não esteja familiarizado com esse padrão. Isso funcionará como um encanto.

var message = (async function() { return await grabUID(uid) })()
Umesh KC
fonte
-1

A resposta aceita do @ slebetmen explica bem por que isso não funciona. Além dos dois padrões apresentados nessa resposta, outra opção é acessar apenas as propriedades assíncronas por meio de um getter assíncrono personalizado. O construtor () pode acionar a criação assíncrona das propriedades, mas o getter verifica se a propriedade está disponível antes de usá-la ou devolvê-la.

Essa abordagem é particularmente útil quando você deseja inicializar um objeto global uma vez na inicialização e deseja fazê-lo dentro de um módulo. Em vez de inicializar no seu index.jse passar a instância nos locais que precisam, bastarequire seu módulo onde quer que o objeto global seja necessário.

Uso

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

Implementação

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;
    }
  }
}
Neal
fonte
-2

As outras respostas estão faltando o óbvio. Simplesmente chame uma função assíncrona do seu construtor:

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>
    `
}
Navigateur
fonte
Como outra resposta "óbvia" aqui , essa não fará o que o programador normalmente espera de um construtor, ou seja, que o conteúdo seja definido quando o objeto for criado.
Dan Dascalescu
2
@DanDascalescu É definido, de forma assíncrona, que é exatamente o que o questionador exige. Seu ponto é que o conteúdo não é definido de forma síncrona quando o objeto é criado, o que não é exigido pela pergunta. É por isso que a pergunta é sobre o uso de wait / async de dentro de um construtor. Demonstrei como você pode chamar o tanto de espera / assíncrono quanto desejar de um construtor chamando uma função assíncrona a partir dele. Eu respondi a pergunta perfeitamente.
Navigatorur
@ Navigatorur, que foi a mesma solução que eu criei, mas os comentários sobre outra pergunta semelhante sugerem que não deve ser feito dessa maneira. O principal problema de ser uma promessa está perdido no construtor, e isso é antipadrão. Você tem alguma referência em que recomenda essa abordagem de chamar uma função assíncrona do seu construtor?
27418 Marklar
1
@ Marklar sem referências, por que você precisa de alguma? Não importa se algo está "perdido" se você não precisar dele em primeiro lugar. E se você precisar da promessa, é trivial adicionar this.myPromise =(no sentido geral), de modo que não seja um antipadrão em qualquer sentido. Há casos perfeitamente válidas para a necessidade de pontapé de saída um algoritmo assíncrono, mediante a construção, que não tem em si valor de retorno, e adicionando um de nós simples mesmo, então quem está aconselhando a não fazer isso é mal-entendido algo
Navigateur
1
Obrigado por reservar um tempo para responder. Eu estava procurando uma leitura mais aprofundada por causa das respostas conflitantes aqui no Stackoverflow. Eu esperava confirmar algumas das melhores práticas para esse cenário.
Marklar
-2

Você deve adicionar uma thenfunção à instância. Promiseirá reconhecê-lo como um objeto thenable com 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)
}
吴浩锋
fonte
innerResolve(this)não funcionará, pois thisainda é um valor utilizável. Isso leva a uma resolução recursiva sem fim.
Bergi 04/12/19