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


169

На даний момент я намагаюся використовувати 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 / очікувати всередині цього? Замість того, щоб вимагати зворотних дзвінків або .then ()?


6
Мета конструктора - виділити вам об’єкт, а потім негайно повернутися. Ви можете бути набагато конкретнішими щодо того , чому саме ви вважаєте, що ваш конструктор повинен бути асинхронним? Тому що ми майже гарантовано вирішили тут проблему XY .
Майк 'Помакс' Камерманс

4
@ Mike'Pomax'Kamermans Це цілком можливо. В основному, мені потрібно запитувати базу даних, щоб отримати метадані, необхідні для завантаження цього елемента. Запит на базу даних - це асинхронна операція, і тому я вимагаю певного способу очікування, щоб це було завершено перед побудовою елемента. Я вважаю за краще не використовувати зворотні дзвінки, оскільки я використовував функцію очікування / асинхронізацію впродовж решти проекту і хотів би зберегти наступність.
Олександр Креггс

@ Mike'Pomax'Kamermans Повний контекст цього клієнта є електронним поштовим клієнтом, де кожен HTML-елемент схожий на <e-mail data-uid="1028"></email>та звідти є заповненим інформацією customElements.define()методом.
Олександр Креггс

Ви майже не хочете, щоб конструктор був асинхронним. Створіть синхронний конструктор, який повертає ваш об'єкт, а потім скористайтеся методом, як .init()робити асинхронні речі. Крім того, оскільки ви є sublcass HTMLElement, надзвичайно ймовірно, що код, що використовує цей клас, не має уявлення, що це асинхронна річ, тому вам, швидше за все, доведеться шукати зовсім інше рішення.
jfriend00

Відповіді:


263

Це ніколи не може працювати.

asyncКлючове слово дозволяє awaitбути використано в функції , поміченої як , asyncале він також перетворює цю функцію в генератор обіцянки. Отже функція, позначена символом async, поверне обіцянку. Конструктор з іншого боку повертає об'єкт, який він будує. Таким чином, у нас виникла ситуація, коли ви хочете повернути об'єкт і обіцянку: неможлива ситуація.

Ви можете використовувати async / await лише там, де ви можете використовувати обіцянки, оскільки вони по суті є синтаксичним цукром для обіцянок. Ви не можете використовувати обіцянки в конструкторі, оскільки конструктор повинен повернути об'єкт, який буде побудовано, а не обіцянку.

Для подолання цього є дві моделі дизайну, обидві придумані до того, як обіцянки були навколо.

  1. Використання init()функції. Це працює трохи як jQuery's .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);
               });
        }
    }

    Реалізація за допомогою асинхронізації / очікування:

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

Примітка. Хоча у наведених вище прикладах ми використовуємо обіцянки для конструктора async, вони не є строго необхідними. Ви так само легко можете написати будівельника, який приймає зворотний дзвінок.


Примітка щодо виклику функцій всередині статичних функцій.

Це не має нічого спільного з конструкторами асинхронізації, але з тим, що ключове слово 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) що означає використання сетера , що і прив’язує і відштовхує init кожного разу, коли нове значення буде пов'язане (і, можливо, під час будівництва теж, але, звичайно, не чекаючи в результаті кодового шляху)
Майк 'Pomax' Камерманс

Вам слід прокоментувати, чому наведена нижче відповідь є недостатньою (якщо вона є). Або звертайтеся до нього інакше.
Гарднер

Мені цікаво, чому 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, ви не можете зателефонувати в межах зворотного зв'язку async.

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


1
@PAStheLoD Я не думаю, що це вирішиться до об'єкта без повернення, однак ви говорите, що це так, я перегляну специфікацію та оновлення ...
Downgoat

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 - це Promise <це>, а оскільки клас не є Promise <T>, я не бачу, як він міг би працювати. (Що ви могли б повернути, окрім "цього"?) Отже, це означає, що обидва повернення не потрібні. (Зовнішнє - трохи гірше, оскільки це призводить до помилки компіляції.)
PAStheLoD

3
@PAStheLoD Так, це обмеження машинопису. Як правило, в JS клас Tповинен повертатися Tпри його конструюванні, але щоб отримати здатність асинхронізації, до якої ми повертаємося, Promise<T>яка вирішується this, але це заплутує typecript. Вам потрібна зовнішня віддача, інакше ви не дізнаєтесь, коли обіцянка закінчується - в результаті такий підхід не буде працювати на 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 розбиратися з викликами події та тіньовим домом:

  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>елемента на сторінці, ви не можете використовувати унікальний ідентифікатор, а потім призначити його купі елементів.


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 кухаря, подаючи cooId в якості аргументу, і інстанція створить об'єкт і заповнить його даними кухаря.
Таким чином, потрібно запустити асинхронний матеріал у конструктор.
Я хотів написати:

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}

Однак скажіть, що ви хочете використовувати цей візерунок на багатьох об'єктах ... ви можете абстрагувати це як декоратор або щось, що ви (багатослівно, тьфу) називаєте після визначення, як 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, навіть екземпляр іншого класу. Отже, ви можете повернути a 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 добре пояснює, чому це не працює. На додаток до двох шаблонів, представлених у цій відповіді, ще одним варіантом є доступ до ваших властивостей асинхронізації лише за допомогою спеціального асинхронного геттера. Конструктор () може потім запустити створення асинхронних властивостей, але getter потім перевіряє, чи є властивість доступною, перш ніж вона використовує або повертає її.

Цей підхід особливо корисний, коли ви хочете ініціалізувати глобальний об'єкт один раз при запуску, і ви хочете зробити це всередині модуля. Замість ініціалізації у вашому 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 всередині конструктора. Я продемонстрував, як можна викликати стільки очікування / асинхронізації, скільки потрібно від конструктора, зателефонувавши з нього на функцію async. Я прекрасно відповів на питання.
Навігатор

@Navigateur це було те саме рішення, що і я придумав, але коментарі до іншого подібного питання припускають, що це не слід робити так. Основна проблема, яка є обіцянкою, втрачається в конструкторі, а це антипатерн. Чи є у вас посилання, де він рекомендує такий підхід викликати функцію асинхронізації від вашого конструктора?
Марклар

1
@Marklar немає посилань, навіщо вам потрібні? Не має значення, чи щось "втрачено", якщо вам це не потрібно в першу чергу. І якщо вам потрібна обіцянка, то тривіально додати 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.