web-dev-qa-db-ja.com

Async / Await Classコンストラクタ

現時点では、クラスコンストラクター関数内でasync/awaitを使用しようとしています。これは、私が取り組んでいるElectronプロジェクト用のカスタム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を使用できるようにする方法はありますか?コールバックや.then()を必要とする代わりに?

71

これは決して動作することはできません。

asyncキーワードを使用すると、awaitとしてマークされている関数でasyncを使用できますが、その関数はプロミスジェネレータにも変換されます。そのため、asyncでマークされた関数は約束を返します。一方、コンストラクタは、それが構成しているオブジェクトを返します。したがって、私たちはあなたが物と約束の両方を返したいという状況を持っています。不可能な状況です。

Promiseは基本的にpromiseの構文糖であるため、async/awaitはpromiseを使用できる場所でしか使用できません。コンストラクタはプロミスではなく、構築するオブジェクトを返さなければならないため、コンストラクタでプロミスを使用することはできません。

これを克服するための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);
        }
    }
    

注:上記の例ではasync Builder用のpromiseを使用していますが、厳密に言えば必要ありません。コールバックを受け付けるビルダーを簡単に書くことができます。


静的関数内で関数を呼び出す際の注意.

これは非同期コンストラクタとは何の関係もありませんが、キーワード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 () {}
}
131
slebetman

あなたできる間違いなくこれをします。基本的に:

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

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

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

クラスを作成するには

let instance = await new AsyncConstructor();

注:superを使用する必要がある場合は、非同期コールバック内で呼び出すことはできません。この解決策はnot100%完璧ですが、私の考えではかなり慣用的であり、私はこれを私のコードで常に使用しています。

44
Downgoat

あなたのコメントに基づいて、あなたはおそらくアセットロードを伴う他のすべての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', ...));
  }

idclassに変更したことにも注意してください。ページ上で<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()で呼び出します。

3
Vidar

このテストケースは、@ 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を見ているように、それらはそれぞれレコードフォーマットに関連付けられています。私の場合はMongoDBコレクションで、例えば料理人のようなものです。
cooksDAOインスタンスは料理人のデータを保持します。
私の落ち着きのない精神では、引数としてcookIdを提供するクックのDAOをインスタンス化することができます。そして、インスタンス化はオブジェクトを作成し、それにクックのデータを追加します。
したがって、コンストラクタに非同期のものを実行する必要があります。
私は書きたかった:

let cook = new cooksDAO( '12345' );  

cook.getDisplayName()のような利用可能なプロパティを持つため。
この解決策で私はしなければなりません:

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

これは理想と非常によく似ています。
また、これをasync関数内で行う必要があります。

私のB-planは、init関数を使うという@slebetmanの提案に基づいて、データのロードをコンストラクターから除外し、次のようにすることでした。

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

これは規則を破るものではありません。

2
Juan Lanus

avoidextendを使用できる場合、クラスをまとめて回避し、constructorsとして関数構成を使用できます。クラスメンバーの代わりにスコープ内の変数を使用できます。

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

シンプルに

const a = await buildA(...);

TypeScriptまたはフローを使用している場合、constructorsのインターフェースを強制することもできます。

Interface A {
  getData: object;
}

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

コンストラクトで非同期メソッドを使用する?

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

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
Jeff Lowery

メッセージを返す匿名非同期関数をすぐに呼び出して、メッセージ変数に設定できます。このパターンに慣れていない場合は、すぐに呼び出される関数式(IEFES)を調べてください。これは魅力のように機能します。

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