JavaScriptの非同期処理とイベントループ
コールスタック
- コールスタックは「いまどの関数が動いていて、その中でどの関数を呼び出したのか」を管理する仕組みです
- スタック構造なので、最後に呼ばれた関数から順番に処理が戻っていきます
シングルスレッド
- JavaScriptはシングルスレッドで動作し、同時に複数の処理を並列に実行できません
- ただし、ブラウザはWeb APIという仕組みを持ち、通信処理やタイマー処理などをバックグラウンドで進めてくれます
例: setTimeout
や fetch
はWeb APIに処理を依頼し、完了後にコールバックキューへ戻されます。
その後、イベントループにより順番が来たときに再びスタックへ積まれて実行されます。
イベントループの流れ(図解)
┌───────────────┐
│ Call Stack │ ← 関数が実行される場所
└───────┬───────┘
│
▼
┌───────────────┐
│ Web API │ ← setTimeout / fetch などが動く
└───────┬───────┘
│
▼
┌───────────────┐
│ Callback Queue │ ← 完了した処理が待機する場所
└───────┬───────┘
│
▼
┌───────────────┐
│ Event Loop │ ← Stack が空なら Queue から実行
└───────┬───────┘
│
▼
(再び Call Stack へ)
コールバック地獄
- 非同期処理をコールバック関数でネストしまくると、可読性が落ちます
- これをコールバック地獄と呼びます
doTask1((result1) => {
doTask2(result1, (result2) => {
doTask3(result2, (result3) => {
console.log("すべて完了:", result3);
});
});
});
見ての通り、ネストが深くなると大変です。
Promise(約束)
-
Promiseは「非同期処理の最終的な成功または失敗」を表すオブジェクトです
-
状態は以下の3つ:
- pending(待機中): 初期状態
- fulfilled(成功): 処理が完了
- rejected(失敗): 処理がエラーで終了
mockRequest("api/data/page1")
.then((res1) => {
console.log("ページ1成功:", res1);
return mockRequest("api/data/page2");
})
.then((res2) => {
console.log("ページ2成功:", res2);
return mockRequest("api/data/page3");
})
.then((res3) => {
console.log("ページ3成功:", res3);
})
.catch((err) => {
console.error("エラー発生:", err);
});
Promiseを使うことで、ネストが浅くなり見通し良くなります。
async / await
-
async関数は必ずPromiseを返す特殊な関数です
- 値を返せば、自動的に
Promise.resolve(value)
になる - エラーを投げれば、
Promise.reject(error)
になる
- 値を返せば、自動的に
-
awaitはPromiseが解決するまで処理を一時停止し、結果を直接受け取れます
async function fetchData() {
try {
const data1 = await mockRequest("api/info/step1");
console.log("step1:", data1);
const data2 = await mockRequest("api/info/step2");
console.log("step2:", data2);
const data3 = await mockRequest("api/info/step3");
console.log("step3:", data3);
} catch (err) {
console.error("処理に失敗:", err);
}
}
これで同期処理のように書けるため、可読性が大幅に向上します。