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

これを回避する方法はありますか?これで非同期/待機を使用できますか?コールバックまたは.then()を要求する代わりに?


6
コンストラクターの目的は、オブジェクトを割り当て、すぐに戻ることです。コンストラクタを非同期にする必要がある理由を具体的に説明できますか?ここでは、XY問題への対処がほぼ保証されているためです。
Mike 'Pomax' Kamermans

4
@ Mike'Pomax'Kamermansそれはかなり可能です。基本的に、この要素をロードするために必要なメタデータを取得するために、データベースを照会する必要があります。データベースのクエリは非同期操作であるため、要素を作成する前に、これが完了するのを待つ何らかの方法が必要です。残りのプロジェクト全体でawait / asyncを使用しており、継続性を維持したいので、コールバックは使用しません。
Alexander Craggs

@ Mike'Pomax'Kamermansこれの完全なコンテキストは電子メールクライアントであり、各HTML要素はと似て<e-mail data-uid="1028"></email>おり、そこからcustomElements.define()メソッドを使用して情報が入力されます。
Alexander Craggs

コンストラクタを非同期にしたくはないでしょう。オブジェクトを返す同期コンストラクターを作成.init()し、非同期の処理を行うようなメソッドを使用します。さらに、HTMLElementをサブクラス化しているため、このクラスを使用するコードが非同期であることを認識していない可能性が非常に高いため、とにかくまったく異なるソリューションを探す必要があります。
jfriend00

回答:


262

これは決して機能しません

asyncキーワードができますawaitとしてマークされた関数で使用されるasyncが、それはまた約束ジェネレータにその機能を変換します。したがって、でマークされた関数はasyncpromiseを返します。一方、コンストラクタは、構築中のオブジェクトを返します。したがって、オブジェクトとプロミスの両方を返す必要がある状況があります。それは不可能な状況です。

async / awaitは、基本的にプロミスの構文シュガーであるため、プロミスを使用できる場所でのみ使用できます。コンストラクターはプロミスではなく、構築するオブジェクトを返す必要があるため、コンストラクターでプロミスを使用することはできません。

これを克服するための2つの設計パターンがあり、どちらも約束がなされる前に発明されました。

  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の最も一般的な回避策の1つです。もちろん、ビルダーパターンは、多くの複雑なパラメーターを必要とするオブジェクトを構築するときに使用されます。これはまさに非同期ビルダーのユースケースです。違いは、非同期ビルダーはオブジェクトを返さず、そのオブジェクトのプロミスを返すことです。

    使用法:

    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()特定の属性に関連付けられた機能を備えています。新しい値がバインドされるたびに(そしておそらく構築中にも、もちろん結果のコードパスを待たずに)initをバインドして開始しますsrchrefdata-uid
Mike 'Pomax' Kamermans

以下の回答では不十分な理由についてコメントしてください(不十分な場合)。またはそれ以外の場合は対処します。
Augie Gardner

bind最初の例でなぜ必要なのcallback.bind(this)();ですか?this.otherFunc()コールバック内などのことができるように?
Alexander Craggs

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に関する注記:コンストラクターがのPromise<MyClass>代わりに型を返すため、TypeScriptで問題が発生しますMyClass。私が知っているこれを解決するための決定的な方法はありません。@blitterが提案する1つの潜在的な方法は、コンストラクター本体/** @type {any} */の先頭に置くことです。ただし、これがすべての状況で機能するかどうかはわかりません。


1
@PAStheLoDリターンなしではオブジェクトに解決されるとは思わないが、それがそうであると言っているので、仕様を確認して更新する...
Downgoat

2
@JuanLanus非同期ブロックは自動的にパラメーターをキャプチャするため、引数xについては、実行する必要があるだけですconstructor(x) { return (async()=>{await f(x); return this})() }
Downgoat

1
@PAStheLoD:return thisが必要です。これはconstructor自動的に行われますが、非同期IIFEでは行われず、空のオブジェクトが返されるためです。
Dan Dascalescu

1
現在TS 3.5.1を対象として、ES5、ES2017、ES2018(およびその他の可能性がありますが、私はチェックしていません)を返すと、コンストラクターでreturnを実行すると、次のエラーメッセージが表示されます。クラスのインスタンスタイプ。」IIFEのタイプはPromise <this>であり、クラスはPromise <T>ではないため、どのように機能するかわかりません。(「これ」以外に何を返すことができますか?)つまり、両方の戻りは不要です。(コンパイルエラーにつながるため、外側の方が少し悪いです。)
PAStheLoD '30

3
@PAStheLoDええ、それはtypescriptの制限です。通常、JSでは、クラスTT構築時に戻る必要がありますが、非同期機能を取得Promise<T>するにthisはに解決されますが、typescriptが混乱します。外側の戻り値が必要です。そうしないと、プロミスがいつ完了するかわからなくなります—その結果、このアプローチはTypeScriptでは機能しません(おそらく、型のエイリアシングによるハックがない限り)。
Typescriptの

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関数がイベント呼び出しとシャドウ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', ...));
  }

また、私はあなたを変え注意idclassあなたにいくつかの奇妙なコードを書いていない限りしかあなたの単一のインスタンスできるため、<e-mail>ページ上の要素は、あなたがユニークな識別子を使用して、要素の束にそれを割り当てることはできません。


2

このテストケースは、@ Downgoatの回答に基づいて作成しました。
NodeJSで実行されます。これは、非同期部分がsetTimeout()呼び出しによって提供されるDowngoatのコードです。

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

私の使用例は、Webアプリケーションのサーバー側のDAOです。
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には型システムがないため、重大な問題が発生します。そのため、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 classesは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(...);

typescriptまたはフローを使用している場合は、コンストラクターのインターフェースを強制することもできます

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の受け入れられた答えは、これがうまくいかない理由をよく説明しています。その回答で提示された2つのパターンに加えて、別のオプションは、カスタム非同期ゲッターを介してのみ非同期プロパティにアクセスすることです。コンストラクター()は、プロパティの非同期作成をトリガーできますが、ゲッターは、プロパティが使用または返される前に、プロパティが使用可能かどうかを確認します。

この方法は、起動時にグローバルオブジェクトを一度初期化し、モジュール内で初期化する場合に特に便利です。で初期化し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>
    `
}

同様に、ここでもう一つの「明白な」答えは、この1は、プログラマが、一般的に、コンストラクタ、つまりオブジェクトが作成されたときにコンテンツが設定されていることを期待するものしないでしょう。
Dan Dascalescu 2018年

2
@DanDascalescuこれは、非同期で設定されます。これは、質問者が正確に要求するものです。問題は、オブジェクトの作成時にコンテンツが同期的に設定されないことです。これは質問では必要ありません。そのため、コンストラクタ内からawait / asyncを使用することが問題になります。コンストラクターから非同期関数を呼び出すことにより、コンストラクターから必要なだけ待機/非同期を呼び出す方法を示しました。私は質問に完全に答えました。
Navigateur

@Navigateurは私が思いついたのと同じ解決策でしたが、別の同様の質問へのコメントはそれがこの方法で行われるべきではないことを示唆しています。約束である主な問題はコンストラクタで失われ、これはアンチパターンです。コンストラクターから非同期関数を呼び出すこのアプローチを推奨する参照がありますか?
Marklar、2018年

1
@Marklar参照なし、なぜ必要なのですか?そもそもそれが必要なければ、何かが「失われた」かどうかは関係ありません。また、約束が必要な場合は、this.myPromise =(一般的な意味で)追加するのは簡単であり、いかなる意味でもアンチパターンではありません。構築時に非同期アルゴリズムを開始する必要がある完全に有効なケースがあり、それ自体は戻り値を持たず、いずれにせよ単純なものを追加するので、これを行わないようにアドバイスしている人は何かを誤解しています
Navigateur

1
返信に時間を割いていただきありがとうございます。Stackoverflowでの答えが矛盾しているため、私は詳細を読んでいました。このシナリオのベストプラクティスのいくつかを確認したいと思っていました。
Marklar、2018年

-2

thenインスタンスに関数を追加する必要があります。自動的にPromiseそれをthenableオブジェクトとして認識し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まだtheableなので、機能しません。これは、終わりのない再帰的な解決につながります。
Bergi、
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.