はじめに - Javaエンジニアの苦悩。JavaScriptの非同期処理にはまる -
JavaScriptの非同期処理、await を書いているのになぜかスルーされる……。そんな経験はありませんか?
わたしは数年前、Javaアプリケーション(アプレットだけど)をTypeScriptに移植する際、
この『止まらない非同期』に数週間も頭を抱えました。
結論から言うと、非同期処理を同期処理のように動かすには、
最下層から最上層まで**『一気通貫』で約束(Promise)を一気通貫で繋ぎ切る必要がありました。
今回の記事は、私が泥臭い試行錯誤の末にたどり着いた、非同期リレーの鉄則を共有します。
状況とJavaプログラマからの感覚
JavaからJavaScript(実際にはTypeScriptだけど)に書き換えをしていくとき、
通信処理部分が非同期処理になる。という話はインフラチームからは聞いていました。
現状、Javaでは同期処理となっているので、その通りにする必要がありました。
その解決策として、Promise(約束)を知りました。
その当時の各サイトの情報や、他チームのソースコードを見て真似て書いてみました。
まずは、サーバー通信処理への部分イメージ
function サーバー通信を待つ関数() {
return new Promise((resolve, reject) => {
// 1. 通信を投げる
通信処理を投げる();
// 2. 終わったかどうかを定期的にチェックする(ポーリング)
const timer = setInterval(() => {
if (通信成功した?) {
clearInterval(timer);
resolve("成功データ");
} else if (タイムアウトした?) {
clearInterval(timer);
reject("エラー");
}
}, 100); // 100ミリ秒ごとに確認
});
}
【Javaエンジニアの視点で】
実際には、手探り状態で必死でした。
「Promiseというものを使えばいいらしい」
「setIntervalで監視すれば待てるはずだ」……。
慣れないTypeScriptと格闘しながら、ようやく書き上げたこの最下層の関数。
「これだけロジックを組んだんだ。
これでもう、Javaの同期処理と同じように、
この関数を呼んだところでプログラムは一旦止まり、
データが返ってくるのを待ってくれるはずだ」
そう、一番下(最下層)さえ「待てる構造」に作り替えれば、
上層階のプログラムたちは何も知らなくても恩恵に預かれると、
本気で信じていたのです。
上層部は「約束」を無視して走り去っていく。。。
最下層プログラムを作りきり、
上位メソッドは現行Javaのロジックを移植して、いざ、処理を呼び出してみました。
フロントエンドは変わっているので、
A(GUI)→B→C(最下層の共通処理)でいうと、Bの部分は移植でいけました。
Aはもちろん新規作成です。
Javaエンジニアの感覚なら、通信が終わるまで止まってくれるはずです。
Bの通信処理
export class B {
public 通信処理B():返信データ {
・・・
// Cが終わるまでここで「待つ」
let 返信データ = C.サーバー通信を待つ関数();
return 返信データ;
}
}
Aのイベント処理(GUI)
イメージとしてはBと変わりはありません。
export class A {
public イベント処理A() {
・・・
// Bが終わるまでここで「待つ」
let 返信データ = B.通信処理B();
// ここでようやく本物のデータを使って貼り付けができる
this.データ貼り付け処理(data);
}
}
これが止まらないんです。
データ貼り付け処理が先に動いてしまうんです。
ここからなぜなぜ地獄にハマりました・・・汗。
解決:最上層から最下層まで「一気通貫」で繋ぎ切る
結論から言うと、JavaScript/TypeScriptの世界で同期処理を実現するには、
**「バトン(Promise)を受け取る全員が、前の走者を待つ(await)」**
というルールを徹底するしかありません。
一箇所でも await を忘れたり、async を抜かしたりすれば、
その瞬間に「一気通貫」は崩壊し、プログラムは走り去ってしまいます。
Bの修正:リレーの中継地点
まずは中間の「B」です。 ここが一番の盲点でした。
自分も「約束(Promise)」を返すように宣言し、下層の結果をしっかり待ちます。
export class B {
// 1. asyncを付けて「非同期(約束を返す)メソッド」であることを宣言
// 2. 戻り値を Promise<返信データ> に包む
public async 通信処理B():Promise<返信データ> {
・・・
// 3. await を付けて、Cが終わるまでここで「止まって待つ」
let 返信データ = await C.サーバー通信を待つ関数();
return 返信データ;
}
}
Aの修正:一気通貫のゴール地点
そして最上層の「A」です。
ここも同様に「待ち」の姿勢を入れなければなりません。
export class A {
public async イベント処理A() {
・・・
// 4. ここでも await。Bが結果を持ってくるまで次の行へ行かせない!
let 返信データ = await B.通信処理B();
// 5. ここでようやく、本物のデータ手に入れることができ、データ貼り付けができる
this.データ貼り付け処理(data);
}
}
まとめ:数週間かけて分かった「一気通貫」の正体
Javaエンジニアの私にとって、
最大の誤算は**「一番下が待てば、上も勝手に待つだろう」**という思い込みでした。
JavaScriptの世界はシビアで厳しいものでした。
「待ってほしいなら、全員が『待つ(await)』と宣言し、
バトン(Promise)を繋ぎ続けなければならない」。
この「一気通貫」の鎖さえ完成すれば、
非同期処理はまるで魔法のように、
Javaの同期処理と同じような顔をして動いてくれるようになります。
もし、あなたの await が無視されているなら、
どこかでその鎖が切れていないか、
数珠つなぎを一から辿ってみてください。
注意! たとえAとCが完璧でも、
中間のBが await を一回忘れるだけで、
Aの手元にはまた「空っぽの箱(Pending状態のPromise)」が届くことになります。
参考情報
Promise を使う(Promise チェーンの解説)
https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Using_promises
「async/await」の公式解説
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/async_function
#TypeScript #async-await