ファミレスでイメージする難しくないJavaScriptの非同期処理:Promise、async/await
はじめに
最近のフロントエンド開発では、このようなNext.js(React)とTypeScriptが組み合わさったコードが当たり前のように使われるようになりましたよね。
// 非同期処理を含むコンポーネントの例
export default async function ArticlePage() {
const article = await fetchArticle(); // データを非同期で取得
return <ArticleCard article={article} />;
}
// 通常のコンポーネント
const ArticleCard = ({ article }: ArticleCardProps) => {
return (
// 各コンポーネントが記述される
);
};
でも、こういったコードがうまく読めなかったり、意味がわからなかったりするのは、JavaScriptの非同期処理の理解がまだ十分でないからかもしれません。
とくに非同期処理なんて、まさに「JavaScriptがよくわからない!!」と感じる一因ではないでしょうか。
非同期処理は複数の処理を同時に進めるために、処理の完了を待たずに次の処理を進める仕組みです。
と言われても、具体的なイメージが湧きにくいことってありますよね。
そこで、この記事では日常生活の例を通じて、非同期処理を少しでもわかりやすくイメージできるようにファミリーレストランでの注文の流れを例にしてみました!
1. 繁盛するファミレスと非同期処理
あるファミリーレストランに、AさんとBさんが来店しました。2人の注文は以下の通りです。
-
Aさん:パンケーキ → コーヒー → イチゴパフェ
「出来立てのパンケーキと一緒に淹れたてのコーヒーを楽しみたい!食後にはイチゴパフェを食べよう♪」 -
Bさん:サラダ → ナポリタン → アイスコーヒー
「まずは、サラダからおなかに入れたいな。メインディッシュのナポリタンを食べたらアイスコーヒーで締めよう。」
さて、この注文をどう処理するか。これがまさにJavaScriptの非同期処理そのものです!
2. コールバック地獄:注文用紙のない厨房
昔のJavaScriptでは、こんな感じでコードを書いていました:
注文する(Aさん_パンケーキ, () => {
注文する(Bさん_サラダ, () => {
注文する(Aさん_コーヒー, () => {
注文する(Bさん_ナポリタン, () => {
注文する(Aさん_イチゴパフェ, () => {
注文する(Bさん_アイスコーヒー, () => {
console.log("全ての注文が完了しました!");
});
});
});
});
});
});
これじゃ、まるで注文用紙のない厨房!どの料理がどのお客さんのものか、出す順番はどうなのか、わからなくなってしまいます。
3. Promise:きちんとした注文表の登場
そこで登場したのがPromise
です。これこそ、効率的な厨房運営の要、注文表そのもの!
function Aさんの注文() {
return 注文する(パンケーキ)
.then(() => 注文する(コーヒー))
.then(() => 注文する(イチゴパフェ))
.catch(エラー => {
if (エラー === "イチゴ切れ") {
return 注文する(チョコバナナパフェ);
}
throw エラー;
});
}
function Bさんの注文() {
return 注文する(サラダ)
.then(() => 注文する(ナポリタン))
.then(() => 注文する(アイスコーヒー))
.catch(エラー => {
if (エラー === "氷切れ") {
return 注文する(ホットコーヒー);
}
throw エラー;
});
}
Promise.all([Aさんの注文(), Bさんの注文()])
.then(() => console.log("全ての注文が完了しました!"))
.catch(エラー => console.log("申し訳ありません、注文に問題が発生しました:", エラー));
これなら、各お客さんの注文の順序もはっきりしていて、エラー(材料切れなど)にも対応できます。
4. async/await:より読みやすい注文表
さらに進化したのがasync/await。これはPromiseをより使いやすくした書き方です。
async function Aさんの注文() {
try {
await 注文する(パンケーキ);
await 注文する(コーヒー);
await 注文する(イチゴパフェ);
} catch (エラー) {
if (エラー === "イチゴ切れ") {
console.log("申し訳ありません。イチゴがないのですが、チョコバナナパフェでもよろしいでしょうか?");
await 注文する(チョコバナナパフェ);
} else {
throw エラー;
}
}
}
async function Bさんの注文() {
try {
await 注文する(サラダ);
await 注文する(ナポリタン);
await 注文する(アイスコーヒー);
} catch (エラー) {
if (エラー === "氷切れ") {
console.log("申し訳ありません。氷を切らしてしまいました。ホットコーヒーでよろしいでしょうか?");
await 注文する(ホットコーヒー);
} else {
throw エラー;
}
}
}
async function 全ての注文を処理する() {
try {
await Promise.all([Aさんの注文(), Bさんの注文()]);
console.log("全ての注文が完了しました!お食事をお楽しみください!");
} catch (エラー) {
console.log("申し訳ありません、注文の処理中に問題が発生しました:", エラー);
}
}
全ての注文を処理する();
まるで通常の料理の手順書のように読めますね!エラーへの対応も自然な流れで書けています。
5. マーメイド図で見る非同期処理
では、AさんとBさんの注文プロセスをマーメイド図で表現してみましょう!
この図を見れば、非同期処理の流れがより明確になるはずです。
この図を見ると、AさんとBさんの注文が同時に処理されていることがわかります。
また、イチゴパフェとアイスコーヒーにおけるエラーハンドリング(材料切れの対応)も視覚的に理解できますね。
複数のタスク(ここでは注文)が同時に進行し、それぞれのタスクで問題が発生した場合の対処法(エラーハンドリング)も組み込まれています。
Promiseやasync/awaitを使うことで、このような複雑な処理フローを効率的に、
そして読みやすく実装できるのです!
まとめ
JavaScriptの非同期処理は、ファミリーレストランのように絶えず人が訪れるお店が効率的に運営する仕組みと同じなんです。
- コールバック = 注文用紙のない混沌とした厨房
- Promise = きちんとした注文表の導入
- async/await = さらに読みやすくなった注文表
そして、エラーハンドリングは、材料切れや機器の不具合といった予期せぬ事態への対応。プログラミングの世界でも、レストランの世界でも、臨機応変な対応が求められるんですね。
余談
私は、この記事を書けるようになるまで「技術的な用語や構文は理解できても、その背後にある概念やロジックが掴めない」ことに悩み続けてきました。
今でこそ、従来の非同期処理が抱える本質的な課題は「結果の順序を制御できなかった」ことであり、Promise
やasync/await
はその問題点を改善したアプローチで、真に知るべき仕組みは根底にあるコールバック関数だったと理解できます。
でも、それはコールバック地獄と呼ばれるものからひとつひとつ書いて、試してという試行錯誤の末に得たものです。
また、JavaScriptのコード解説で実際に見る形としては、以下のようなものでしょう。
// お客さんの注文を受け付ける
function takeOrder(menuItem) {
console.log(`${menuItem}のご注文を承りました`);
// 料理を作る時間(仮に3秒とする)
setTimeout(() => {
console.log(`${menuItem}のご用意ができました`);
}, 3000);
}
takeOrder('パンケーキ');
しかし、このようなコードをたくさん書いても、読んでも、私はいまいち理解ができませんでした。だから、別のなにかに例えてみようと考えたんです。
-
setTimeout
「注文を受けてから一定時間後に料理を提供する」 -
onchenge
「お客さんがメニューを変更したときの対応」 -
onsubmit
「ウェイターが注文内容を再確認するときの対応」 -
action
「注文した料理がそれぞれ何番のテーブルに運ばれるかを指定する」
みたいな具合にすると理解しやすくなりました!
皆さんも、難しい概念を理解しようとするときは、身近なものに例えてみてはいかがでしょうか?
きっと、たくさんの発見があることでしょう。
追記
この記事の続編として、「イベントハンドラーとエラーハンドリング」をテーマにした記事を準備中です。
非同期処理の理解をさらに深めたい方は、ぜひそちらもチェックしてみてください!
Discussion