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


170

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


6
Цель конструктора - выделить вам объект и сразу же вернуться. Можете ли вы быть более точным в отношении того , почему ваш конструктор должен быть асинхронным? Потому что мы почти гарантированно имеем дело с проблемой XY .
Майк 'Pomax' Камерманс

4
@ Mike'Pomax'Kamermans Это вполне возможно. По сути, мне нужно запросить базу данных, чтобы получить метаданные, необходимые для загрузки этого элемента. Запрос к базе данных является асинхронной операцией, и, следовательно, мне требуется некоторый способ ожидания завершения этого процесса перед созданием элемента. Я бы предпочел не использовать обратные вызовы, так как я использовал await / async в остальной части проекта и хотел бы сохранить преемственность.
Александр Краггс

@ Mike'Pomax'Kamermans Полный контекст этого - почтовый клиент, где каждый элемент HTML выглядит примерно так, <e-mail data-uid="1028"></email>и оттуда заполняется информацией с использованием customElements.define()метода.
Александр Краггс

Вы в значительной степени не хотите, чтобы конструктор был асинхронным. Создайте синхронный конструктор, который возвращает ваш объект, а затем используйте метод, подобный .init()асинхронному. Кроме того, поскольку вы используете подкласс HTMLElement, весьма вероятно, что код, использующий этот класс, не знает, что это асинхронная вещь, поэтому вам, скорее всего, придется искать совершенно другое решение.
jfriend00

Ответы:


264

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

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

обратите внимание, что, основываясь на комментариях, идея заключается в том, что это HTML-элемент, который обычно не имеет руководства, init()но имеет функциональность, привязанную к какому-либо конкретному атрибуту, например srcили href(и в данном случае, data-uid), что означает использование установщика, который одновременно связывает и запускает инициализацию каждый раз, когда связывается новое значение (и, возможно, также во время конструирования, но, конечно, без ожидания пути к результирующему коду)
Майк 'Pomax' Kamermans

Вам следует прокомментировать, почему приведенного ниже ответа недостаточно (если он есть). Или обратитесь к нему иначе.
Оги Гарднер

Мне любопытно, почему bindтребуется в первом примере callback.bind(this)();? Так что вы можете делать такие вещи, как this.otherFunc()в обратном вызове?
Александр Краггс

1
@AlexanderCraggs Это просто удобство, что thisв обратном вызове относится myClass. Если вы всегда используете myObjвместо thisвас, это не нужно
Slebetman

1
В настоящее время существует ограничение на язык, но я не понимаю, почему в будущем у вас не может быть так const a = await new A()же, как у нас есть обычные функции и асинхронные функции.
7ynk3r

138

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

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

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

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

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

let instance = await new AsyncConstructor();

Это решение имеет несколько недостатков:

superпримечание : если вам нужно использовать super, вы не можете вызвать его в асинхронном обратном вызове.

Примечание TypeScript: это вызывает проблемы с TypeScript, потому что конструктор возвращает типPromise<MyClass> вместо MyClass. Нет определенного способа решить эту проблему, о которой я знаю. Один из возможных способов, предложенных @blitter, заключается в том, чтобы поместить /** @type {any} */в начало тело конструктора - однако я не знаю, работает ли это во всех ситуациях.


1
@PAStheLoD Я не думаю, что это разрешится к объекту без возврата, но вы говорите, что это так, я рассмотрю спецификацию и
обновлю

2
@JuanLanus асинхронный блок будет автоматически захватывать параметры, поэтому для аргумента x вам нужно только сделатьconstructor(x) { return (async()=>{await f(x); return this})() }
Downgoat

1
@PAStheLoD: return thisнеобходим, потому что, хотя constructorон делает это автоматически для вас, этого не делает асинхронный IIFE, и вы в конечном итоге вернете пустой объект.
Дан Даскалеску

1
В настоящее время в TS 3.5.1 для ES5, ES2017, ES2018 (и, возможно, других, но я не проверял), если вы делаете возврат в конструкторе, вы получаете следующее сообщение об ошибке: «Возвращаемый тип сигнатуры конструктора должен быть назначен для Тип экземпляра класса. " Тип IIFE - это Обещание <this>, а поскольку класс не является Обещанием <T>, я не понимаю, как это могло бы работать. (Что вы могли бы вернуть, кроме «это»?) Так что это означает, что оба возврата не нужны. (Внешний немного хуже, так как это приводит к ошибке компиляции.)
PAStheLoD

3
@PAStheLoD да, это ограничение машинописи. Обычно в JS класс Tдолжен возвращаться Tпри создании, но чтобы получить возможность асинхронности, которую мы возвращаем, Promise<T>которая разрешается this, но это сбивает с толку машинопись. Вам действительно нужен внешний возврат, иначе вы не будете знать, когда обещание завершится - в результате этот подход не будет работать с TypeScript (если только не будет хака с возможно псевдонимом типа?). Не специалист по машинописи, хотя и не могу говорить об этом
Downgoat

7

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

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

Основываясь на ваших комментариях, вы, вероятно, должны делать то, что делает любой другой 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на a class, потому что, если вы не напишите какой-то странный код, разрешающий только один экземпляр вашего <e-mail>элемента на странице, вы не сможете использовать уникальный идентификатор, а затем назначить его группе элементов.


2

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

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


2

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

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

Решение для временной задержки

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

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

Однако возникает серьезная проблема, потому что в ES нет системы типов, поэтому, если вы забудете вызвать ее, вы только что вернулись undefined потому что конструктор ничего не возвращает. К сожалению. Намного лучше было бы сделать что-то вроде:

Лучшее, что можно сделать:

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}

Фабричный метод решения (чуть лучше)

Однако тогда вы можете случайно сделать новый AsyncOnlyObject, вам, вероятно, следует просто создать фабричную функцию, которая использует Object.create(AsyncOnlyObject.prototype)напрямую:

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

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

Однако, скажем, вы хотите использовать этот шаблон на многих объектах ... вы можете абстрагировать его как декоратор или что-то, что вы (многословно, тьфу) вызываете после определения like postProcess_makeAsyncInit(AsyncOnlyObject), но здесь я собираюсь использовать, extendsпотому что он как бы вписывается в семантику подкласса. (подклассы являются родительским классом + дополнительный, в том смысле, что они должны подчиняться контракту разработки родительского класса и могут делать дополнительные вещи; асинхронный подкласс был бы странным, если бы родитель не был также асинхронным, потому что его нельзя было инициализировать одинаково путь):


Абстрактное решение (расширяет / версия подкласса)

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}

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


2

В отличие от других, вы можете заставить его работать.

JavaScript classможет возвращать буквально все что угодно constructor, даже экземпляр другого класса. Таким образом, вы можете вернуть Promiseконструктор вашего класса, который разрешает его фактический экземпляр.

Ниже приведен пример:

export class Foo {

    constructor() {

        return (async () => {

            // await anything you want

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

Затем вы создадите экземпляры Fooследующим образом:

const foo = await new Foo();

1

Если вы можете избежать этого 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> { ... }
...

0

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

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

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

-1

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

-2

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

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

Как и другой «очевидный» ответ здесь , он не будет делать то, что программист обычно ожидает от конструктора, то есть, что содержимое устанавливается при создании объекта.
Дан Даскалеску

2
@DanDascalescu Устанавливается асинхронно, что и требуется для спрашивающего. Ваша точка зрения заключается в том, что содержимое не устанавливается синхронно при создании объекта, что не требуется для вопроса. Вот почему вопрос об использовании await / async из конструктора. Я продемонстрировал, как вы можете вызывать из конструктора столько await / async, сколько захотите, вызывая из него асинхронную функцию. Я отлично ответил на вопрос.
Навигатор

@Navigateur, это было то же решение, которое я придумал, но комментарии к другому подобному вопросу говорят о том, что этого делать не следует. Основная проблема в том, что обещание теряется в конструкторе, и это антипаттерн. Есть ли у вас ссылки, где он рекомендует этот подход вызова асинхронной функции из вашего конструктора?
Марклар

1
@ Марклар без ссылок, зачем тебе это? Неважно, если что-то «потеряно», если вам это не нужно. И если вам нужно обещание, добавить тривиальноthis.myPromise = (в общем смысле) в любом смысле не быть анти-паттерном. Существуют совершенно обоснованные случаи необходимости запуска асинхронного алгоритма при создании, который сам по себе не имеет возвращаемого значения, и добавления одного простого значения в любом случае, так что тот, кто советует не делать этого, что-то неправильно понимает
Navigateur

1
Спасибо что нашли время ответить. Я искал дальнейшее чтение из-за противоречивых ответов здесь на Stackoverflow. Я надеялся подтвердить некоторые из лучших практик для этого сценария.
Марклар

-2

Вы должны добавить 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)
}

innerResolve(this)не будет работать, как thisэто все еще возможно. Это приводит к бесконечному рекурсивному разрешению.
Берги
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.