読み込み中...
読み込み中...
読み込み中...
読み込み中...
読み込み中...
JavaScriptはシングルスレッドで動作します。つまり、一度に1つの処理しか実行できません。それにもかかわらずブラウザが固まらずに動作できるのは、イベントループという仕組みのおかげです。
非同期処理(タイマー、ネットワークリクエスト、ファイル読み込みなど)はブラウザやNode.jsのWeb APIに委譲され、完了するとコールバック関数がタスクキューに追加されます。イベントループはコールスタックが空になるたびにタスクキューからコールバックを取り出して実行します。
| 構成要素 | 役割 |
|---|---|
| コールスタック | 現在実行中の関数を積み上げるスタック |
| Web API | タイマー・XHR・DOM イベントなどの非同期処理を担当 |
| タスクキュー(マクロタスク) | setTimeout、setInterval のコールバック |
| マイクロタスクキュー | Promise の then/catch、queueMicrotask |
| イベントループ | コールスタックが空のときにキューからタスクを取り出す |
マイクロタスクとマクロタスクの実行順序
// イベントループの動作順序
console.log("1: 同期処理");
setTimeout(() => {
console.log("2: マクロタスク(setTimeout)");
}, 0);
Promise.resolve().then(() => {
console.log("3: マイクロタスク(Promise)");
});
console.log("4: 同期処理");
// 出力順序:
// 1: 同期処理
// 4: 同期処理
// 3: マイクロタスク(Promise)
// 2: マクロタスク(setTimeout)非同期処理をコールバックだけで書くと、処理が入れ子になりコードの可読性が大きく低下します。この問題は**コールバック地獄(Pyramid of Doom)**と呼ばれ、Promiseやasync/awaitが登場した大きな理由の一つです。
コールバック地獄の問題
// コールバック地獄の例
function fetchUser(id, callback) {
setTimeout(() => callback({ id, name: "田中" }), 100);
}
function fetchPosts(userId, callback) {
setTimeout(() => callback([{ title: "記事1" }]), 100);
}
function fetchComments(postId, callback) {
setTimeout(() => callback([{ text: "コメント1" }]), 100);
}
// ネストが深くなる(コールバック地獄)
fetchUser(1, (user) => {
fetchPosts(user.id, (posts) => {
fetchComments(posts[0].title, (comments) => {
console.log(user, posts, comments);
// さらにネストが続く...
});
});
});チャレンジ
以下のコードの出力順序を予測してください。変数 order に出力順序を配列で格納してください。
ポイント
JavaScriptはシングルスレッドですが、イベントループにより非同期処理を効率的に扱えます。実行順序は「同期処理 → マイクロタスク(Promise等) → マクロタスク(setTimeout等)」です。コールバックの入れ子が深くなるコールバック地獄を避けるために Promise が導入されました。
Promise は非同期処理の結果を表すオブジェクトです。「いずれ完了する(または失敗する)処理」をオブジェクトとして扱えるため、コールバックの入れ子を避け、チェーン形式で処理を記述できます。
Promise は以下の3つの状態を持ちます。
| 状態 | 説明 | 遷移先 |
|---|---|---|
| pending(待機) | 初期状態。まだ結果が確定していない | fulfilled または rejected |
| fulfilled(履行) | 処理が成功して値が確定 | なし(不可逆) |
| rejected(拒否) | 処理が失敗してエラーが確定 | なし(不可逆) |
一度 fulfilled または rejected になると、その状態は変わりません(不可逆)。
Promise の作成と then/catch/finally チェーン
// Promise の作成
const fetchData = new Promise((resolve, reject) => {
const success = true;
setTimeout(() => {
if (success) {
resolve({ id: 1, name: "データ" });
} else {
reject(new Error("取得に失敗しました"));
}
}, 100);
});
// then / catch / finally チェーン
fetchData
.then(data => {
console.log("成功:", data.name); // "成功: データ"
return data.id; // 次の then に渡す
})
.then(id => {
console.log("ID:", id); // "ID: 1"
})
.catch(error => {
console.error("エラー:", error.message);
})
.finally(() => {
console.log("処理完了"); // 成功・失敗問わず実行
});チャレンジ
引数 ms ミリ秒後に value を解決する Promise を返す関数 delay を実装してください。
ポイント
Promise は非同期処理の結果を表すオブジェクトで、pending → fulfilled(成功)または rejected(失敗)に遷移します。then でチェーンし、catch でエラーを捕捉、finally で後処理を行います。状態遷移は不可逆です。
async / await は Promise をベースとした非同期処理をより直感的に書くための構文です。ES2017 で導入されました。
async 関数は常に Promise を返します。関数内で await キーワードを使うと、Promise が解決されるまで処理を一時停止し、解決された値を取得できます。これにより、非同期処理を同期処理のように読みやすく書けます。
| 特徴 | 説明 |
|---|---|
async function | 常に Promise を返す関数を定義 |
await | Promise の解決を待つ(async 関数内でのみ使用可能) |
| エラーハンドリング | try/catch で同期処理と同じように書ける |
| 戻り値 | return した値は Promise.resolve() でラップされる |
Promise チェーンと async/await の比較
// Promise チェーンと async/await の比較
// Promise チェーン版
function fetchUserPromise(id) {
return fetch(`/api/users/${id}`)
.then(res => res.json())
.then(user => fetch(`/api/posts?userId=${user.id}`))
.then(res => res.json())
.catch(err => console.error(err));
}
// async/await 版(同じ処理がより読みやすい)
async function fetchUserAsync(id) {
try {
const userRes = await fetch(`/api/users/${id}`);
const user = await userRes.json();
const postsRes = await fetch(`/api/posts?userId=${user.id}`);
const posts = await postsRes.json();
return posts;
} catch (err) {
console.error(err);
}
}await を連続して書くと逐次実行になります。互いに依存しない非同期処理を高速化するには Promise.all と組み合わせて並列実行にすることが重要です。
逐次実行と並列実行の違い
function wait(ms) {
return new Promise(r => setTimeout(r, ms));
}
// 逐次実行: 合計 300ms かかる
async function sequential() {
await wait(100);
await wait(100);
await wait(100);
console.log("逐次完了");
}
// 並列実行: 最長の 100ms で完了
async function parallel() {
await Promise.all([
wait(100),
wait(100),
wait(100)
]);
console.log("並列完了");
}チャレンジ
async 関数 processData を実装してください。引数の配列 items を受け取り、各要素を2倍にした結果を返す非同期関数です。内部で doubleAsync(100ms 後に値を2倍にして返す)を使ってください。全ての要素を並列に処理してください。
ポイント
async/await は Promise をベースとした構文糖衣で、非同期処理を同期的に読めるコードで書けます。try/catch でエラーハンドリングし、Promise.all と組み合わせて並列実行を実現できます。
ES2015 以降、複数の Promise をまとめて処理するための静的メソッドが追加されてきました。用途に応じて使い分けることで、効率的な非同期処理が実現できます。
| メソッド | 導入 | 解決条件 | 拒否条件 | ユースケース |
|---|---|---|---|---|
Promise.all | ES2015 | 全て成功 | 1つでも失敗 | 全データが必要な場合 |
Promise.race | ES2015 | 最初に完了 | 最初に失敗 | タイムアウト実装 |
Promise.allSettled | ES2020 | 全て完了(成功・失敗問わず) | 拒否しない | 全結果を知りたい場合 |
Promise.any | ES2021 | 最初に成功 | 全て失敗 | フォールバック処理 |
これらのメソッドは全て Promise の配列(正確には Iterable)を受け取り、新しい Promise を返します。
4つの Promise 並行処理メソッド
const fast = new Promise(r => setTimeout(() => r("fast"), 100));
const slow = new Promise(r => setTimeout(() => r("slow"), 300));
const fail = new Promise((_, reject) =>
setTimeout(() => reject(new Error("failed")), 200)
);
// Promise.all: 全て成功するか、1つでも失敗したら reject
Promise.all([fast, slow])
.then(results => console.log("all:", results));
// all: ["fast", "slow"]
// Promise.race: 最初に完了(成功 or 失敗)した結果
Promise.race([fast, slow])
.then(result => console.log("race:", result));
// race: "fast"
// Promise.allSettled: 全ての結果(成功・失敗問わず)
Promise.allSettled([fast, fail, slow])
.then(results => console.log("allSettled:", results));
// allSettled: [
// { status: "fulfilled", value: "fast" },
// { status: "rejected", reason: Error("failed") },
// { status: "fulfilled", value: "slow" }
// ]
// Promise.any: 最初に成功した結果
Promise.any([fail, fast, slow])
.then(result => console.log("any:", result));
// any: "fast"race によるタイムアウト実装と any によるフォールバック
// 実用例: タイムアウト付き fetch
function fetchWithTimeout(url, timeoutMs) {
const fetchPromise = fetch(url).then(r => r.json());
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error("タイムアウト")), timeoutMs)
);
return Promise.race([fetchPromise, timeoutPromise]);
}
// 実用例: 複数 API から最初に応答したものを使う
function fetchFromMirrors(mirrors) {
return Promise.any(
mirrors.map(url => fetch(url).then(r => r.json()))
);
}チャレンジ
Promise.allSettled を使って、成功した Promise の値だけを配列で返す関数 getSuccessfulResults を実装してください。
ポイント
Promise.all(全て成功)、Promise.race(最初の完了)、Promise.allSettled(全結果取得)、Promise.any(最初の成功)を用途に応じて使い分けましょう。タイムアウトやフォールバックなど実用的なパターンが実現できます。
実務での活用
Webアプリケーションでは、サーバーからのデータ取得、画像の読み込み、ファイルの保存など、時間のかかる処理が頻繁に発生します。async/await を使えば「データ取得中はローディング表示→完了したら結果を表示→失敗したらエラーメッセージ」という実用的な画面遷移をシンプルに実装できます。
用語