Конструктор Класса Async/Await

на данный момент я пытаюсь использовать async/await внутри функции конструктора класса. Это для того, чтобы я мог получить custom e-mail тег для электронного проекта, над которым я работаю.

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

на данный момент, однако, проект не работает, со следующей ошибкой:

Class constructor may not be an async method

есть ли способ обойти это, чтобы я мог использовать async/await в этом? Вместо того, чтобы требовать обратных .тогда()?

9 ответов


это может никогда работа.

на async слова await для использования в функции, помеченной как async но он также преобразует эту функцию в генератор обещание. Поэтому функция помеченные async возвращает обещание. Конструктор с другой стороны возвращает объект строительства. Таким образом, у нас есть ситуация, когда вы хотите вернуть объект и обещание: невозможная ситуация.

вы можете использовать только async / await где вы можете использовать обещания, потому что они по сути являются синтаксическим сахаром для обещаний. Вы не можете использовать обещания в конструкторе, потому что конструктор должен возвращать создаваемый объект, а не обещание.

есть два Шаблона дизайна, чтобы преодолеть это, оба изобретены до обещаний были вокруг.

  1. использование


вы can наверняка это сделать. В основном:

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

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

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

для создания класса используйте:

let instance = await new AsyncConstructor();

Примечание: Если вам нужно использовать super, вы не можете вызвать его в асинхронном обратном вызове. Вы должны назвать его вне его, так что это решение не 100% идеально, но на мой взгляд это очень идиоматические и я использую его все время в мой код.


основываясь на ваших комментариях, вы, вероятно, должны сделать то, что делает каждый другой HTMLElement с загрузкой активов: заставить конструктор начать действие боковой загрузки, генерируя событие загрузки или ошибки в зависимости от результата.

Да, это означает использование обещаний, но это также означает "делать то же самое, что и любой другой элемент HTML", поэтому вы в хорошей компании. Например:

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

это запускает асинхронную нагрузку исходного актива, которая, когда это удается, заканчивается onload и когда это случится, заканчивается onerror. Итак, сделайте свой собственный класс тоже так:

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

и затем вы делаете функции renderLoaded/renderError с вызовами событий и 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', ...));
  }

также обратите внимание, что я изменил ваш id до class, потому что, если вы не напишете какой-то странный код, чтобы разрешить только один экземпляр вашего <e-mail> элемент на странице, вы не можете использовать уникальный идентификатор, а затем назначить его на кучу элементов.


поскольку асинхронные функции являются обещаниями, вы можете создать статическую функцию в своем классе, которая выполняет асинхронную функцию, которая возвращает экземпляр класса:

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

вызов с let yql = await Yql.init() из асинхронной функции.


изменение шаблона builder, используя 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;
}

@slebetmen принятый ответ хорошо объясняет, почему это не работает. В дополнение к двум шаблонам, представленным в этом ответе, другим вариантом является доступ только к вашим асинхронным свойствам через пользовательский асинхронный геттер. Конструктор () может затем инициировать асинхронное создание свойств, но геттер затем проверяет, доступно ли свойство, прежде чем использовать или возвращать его.

этот подход особенно полезен, когда вы хотите инициализировать глобальный объект один раз на запуск, и вы хотите сделать это внутри модуля. Вместо инициализации в вашем index.js и передача экземпляра в тех местах, где это необходимо, просто require ваш модуль везде, где необходим глобальный объект.

использование

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

реализация

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

Я сделал этот тестовый случай на основе ответа @Downgoat.
Он работает на NodeJS. Это код Downgoat, где асинхронная часть предоставляется setTimeout() звонок.

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

мой вариант использования-DAOs для серверной части веб-приложения.
Как я вижу DAOs, каждый из них связан с форматом записи, в моем случае коллекция MongoDB, например, повар.
Экземпляр cooksDAO содержит данные повара.
В моем беспокойном уме я мог бы создать экземпляр DAO Кука предоставляет cookId в качестве аргумента, и экземпляр создаст объект и заполнит его данными Кука.
Таким образом, необходимо запустить асинхронный материал в конструктор.
Я хотел написать:

let cook = new cooksDAO( '12345' );  

иметь свойства, такие как cook.getDisplayName().
С этим решением я должен сделать:

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

что очень похоже на идеал.
Кроме того, мне нужно сделать это внутри


в других ответах отсутствует очевидное. Просто вызовите асинхронную функцию из своего конструктора:

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

вы должны добавить