現在、クラスのコンストラクタ関数の中で、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()を必要とする代わりに?
これでは、絶対にうまくいきません。
キーワード async
は async
とマークされた関数の中で await
を使えるようにしますが、同時にその関数をプロミスジェネレータに変換します。つまり、async
でマークされた関数はプロミスを返します。一方、コンストラクタは、構築中のオブジェクトを返します。したがって、オブジェクトとプロミスの両方を返したいというのは、不可能な状況です。
async/awaitは、基本的にプロミスのシンタックスシュガーであるため、プロミスを使用できる場所でしか使用できません。コンストラクタは構築されるオブジェクトを返さなければならず、プロミスを返してはならないので、コンストラクタでプロミスを使うことはできません。
この問題を解決するためのデザインパターンが2つありますが、どちらもプロミスが登場する前に考案されたものです。
1.1. init()
関数の使用。これは、jQueryの.ready()
と同じような働きをします。作成したオブジェクトは、そのオブジェクト自身の init
または ready
関数の中でのみ使用することができます。
使い方。
var myObj = new myClass();
myObj.init(function() {)
// ここでは、myObjを使用できます。
});
実装しています。
クラスmyClass{
コンストラクタ () {
}
init (callback) {
// 非同期で何かを行い、コールバックを呼び出します。
callback.bind(this)();
}
}
2.ビルダーを使う。javascriptではあまり見たことがありませんが、Javaではオブジェクトを非同期に構築する必要がある場合によく使われる回避策の1つです。もちろん、ビルダーパターンは、多くの複雑なパラメータを必要とするオブジェクトを構築するときに使用されます。これはまさに非同期ビルダーの使用例です。非同期ビルダーの違いは、オブジェクトを返すのではなく、そのオブジェクトの約束を返すことです。
使い方。
myClass.build().then(function(myObj) {)
// myObj は約束によって返されます。
// コンストラクタではなく
// あるいはビルダー
});
// async/awaitを使った場合。
async function foo () {
var myObj = await myClass.build();
}
実装です。
クラスmyClass{
コンストラクタ (async_param) {.
if (typeof async_param === 'undefined') { 。
throw new Error('Cannot be called directly');
}
}
スタティックビルド() {
リターン doSomeAsyncStuff()
.then(function(async_result){)
return new myClass(async_result)。
});
}
}
async/awaitを使った実装です。
クラスmyClass {
コンストラクタ (async_param) { 以下のようになります。
if (typeof async_param === 'undefined') { 。
throw new Error('Cannot be called directly');
}
}
スタティックなasyncビルド() {
var async_result = await doSomeAsyncStuff();
return new myClass(async_result);
}
}
Note: 上記の例では、非同期ビルダーにプロミスを使用していますが、厳密には必要ありません。コールバックを受け取るビルダーも簡単に書くことができます。
これは非同期コンストラクタとは全く関係なく、キーワード 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 () {}
}
アセットローディングを行う他のすべてのHTMLElementと同様に、コンストラクタでsideloadingアクションを開始し、その結果に応じてloadイベントまたはerrorイベントを生成するようにしてください。
それは約束事を使うことでもありますが、他の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
をclass
に変更したことにも注意してください。これは、ページ上で<e-mail>
要素のインスタンスを1つしか許可しないような奇妙なコードを書かない限り、一意の識別子を使用して、それをたくさんの要素に割り当てることはできないからです。
インスタンスに then
関数を追加する必要があります。これにより、Promise
は Promise.resolve
で thenable オブジェクトとして自動的に認識されます。
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)
}