Async/Await Class Constructor

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

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/wait внутри этого? Вместо того, чтобы требовать обратные вызовы или .then()?

Ответ 1

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

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

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

Чтобы преодолеть это, есть две модели проектирования, обе они были изобретены до того, как обещания были выполнены.

  1. Использование функции init(). Это работает немного как jQuery .ready(). Созданный вами объект можно использовать только внутри собственной функции init или ready:

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

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

    Реализация:

    class myClass {
        constructor () {
    
        }
    
        init (callback) {
            // do something async and call the callback:
            callback.bind(this)();
        }
    }
    
  2. Используйте строителя. Я не видел, чтобы это часто использовалось в javascript, но это один из наиболее распространенных способов обхода в Java, когда объект должен быть создан асинхронно. Конечно, шаблон построителя используется при построении объекта, который требует много сложных параметров. Это именно тот случай использования для асинхронных сборщиков. Разница в том, что асинхронный построитель не возвращает объект, а обещание этого объекта:

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

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

    Реализация:

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

    Реализация с 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);
        }
    }
    

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


Примечание о вызове функций внутри статических функций.

Это не имеет ничего общего с асинхронными конструкторами, но с тем, что ключевым словом this на самом деле означает (что может быть немного удивительно для людей, приезжающих из языков, которые делают автоматическое разрешение имен методов, то есть языки, которые не нуждаются в this ключевом слове).

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

То есть в следующем коде:

class A {
    static foo () {}
}

Вы не можете сделать:

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

вместо этого вам нужно назвать это как:

A.foo();

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

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

Чтобы исправить это, вы можете сделать bar либо обычной функцией, либо статическим методом:

function bar1 () {}

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

    static bar2 () {}
}

Ответ 2

Вы определенно можете сделать это. В принципе:

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

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

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

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

let instance = await new AsyncConstructor();

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

Ответ 3

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

Да, это означает использование promises, но это также означает "делать вещи так же, как и любой другой элемент 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 для вызовов событий и теневого 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> на странице, вы не можете использовать уникальный идентификатор и затем назначьте ему кучу элементов.

Ответ 4

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

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() из асинхронной функции.

Ответ 5

Я сделал этот тест на основе ответа @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 );

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

let cook = new cooksDAO( '12345' );  

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

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

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

Мой B-план состоял в том, чтобы оставить загрузку данных вне конструктора, основываясь на предложении @slebetman использовать функцию init и сделать что-то вроде этого:

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

который не нарушает правила.

Ответ 6

использовать асинхронный метод в конструкции???

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

Ответ 7

Вы также можете создать самозаверяющую асинхронную анонимную функцию в своем конструкторе:

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

Ответ 8

Если вы можете избежать extend, вы можете избежать всех классов вместе и использовать композицию функций в качестве конструкторов. Вы можете использовать переменные в области вместо членов класса:

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

и просто используйте его как

const a = await buildA(...);

Если вы используете машинописный текст или поток, вы можете даже использовать интерфейс конструкторов

Interface A {
  getData: object;
}

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

Ответ 9

Изменение шаблона построения с использованием 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;
}

Ответ 10

Вы можете немедленно вызвать анонимную асинхронную функцию, которая возвращает сообщение, и установить для нее переменную сообщения. Возможно, вы захотите взглянуть на немедленно вызванные выражения функций (IEFES), если вы не знакомы с этим шаблоном. Это будет работать как шарм.

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

Ответ 11

Вы можете создать метод async init() {... return this;}, а затем делать new MyClass().init() всякий раз, когда вы обычно просто говорите new MyClass().

Это не чисто, потому что оно зависит от всех, кто использует ваш код, и от вас самих, чтобы всегда создавать экземпляр объекта таким образом. Однако, если вы используете этот объект только в определенном месте или двух в вашем коде, это должно быть хорошо.

Ответ 12

Принятый ответ @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;
    }
  }
}

Ответ 13

then вы должны добавить функцию к экземпляру. Promise распознает его как доступный объект с Promise.resolve автоматически

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

Ответ 14

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

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