🤔 はじめに:非同期って何?コーヒーショップで考えよう
JavaScriptのような「シングルスレッド」の言語は、一度に一つのことしかできません。想像してみてください:
あなたは一人でコーヒーショップを経営しています。お客さんAが注文したラテを作っている間、他のお客さんは全員待たなければなりません。営業は大失敗。破産確定です...😱
非同期処理はこの問題を解決します!あなたは魔法の「後でやる」能力を手に入れました。
お客さんAが注文:「ラテください」
あなた:「かしこまりました!(コーヒーマシンをセット)できあがるまで少々お待ちください」
その間に、お客さんBの注文も受けられる!
コーヒーマシンから「ピンポーン🔔」と音がしたら、ラテを渡す
つまり非同期処理とは、「完了を待たずに次の作業に進み、完了したらその結果を使う」テクニックなのです。
👴 第一世代:コールバック関数(2010年頃)
—「終わったら電話してね」方式
// 昔ながらのコールバック方式
function コーヒーを注文する(コーヒーの種類, コールバック) {
console.log(`${コーヒーの種類}を準備中...⏳`);
// コーヒーが完成する時間をシミュレート
setTimeout(() => {
console.log(`${コーヒーの種類}ができました!☕`);
コールバック(`熱々の${コーヒーの種類}`);
}, 2000); // 2秒かかる
}
// 使ってみよう!
console.log("🏪 コーヒーショップへようこそ!");
コーヒーを注文する("カフェラテ", (完成したコーヒー) => {
console.log(`お客さん:${完成したコーヒー}をいただきました!😋`);
});
console.log("レジ係:次のお客さんどうぞ~"); // ここが先に実行される!
実行結果:
🏪 コーヒーショップへようこそ!
カフェラテを準備中...⏳
レジ係:次のお客さんどうぞ~
カフェラテができました!☕
お客さん:熱々のカフェラテをいただきました!😋
🤮 コールバック地獄 —「ロシアンネスティングドール」問題
朝食を作る(() => {
歯を磨く(() => {
着替える(() => {
通勤する(() => {
仕事する(() => {
昼食を食べる(() => {
// もう見るのも嫌...😵
});
});
});
});
});
});
これでは「インデントおじさん」になってしまいます。そこで...
🤵 第二世代:Promise(2015年〜)
—「予約券」方式
// モダンなPromise方式
function コーヒーを注文する(コーヒーの種類) {
return new Promise((resolve, reject) => {
console.log(`${コーヒーの種類}を準備中...⏳`);
if (コーヒーの種類 === "存在しないコーヒー") {
reject(new Error("そんなコーヒーはメニューにありません!"));
return;
}
// コーヒーが完成する時間をシミュレート
setTimeout(() => {
console.log(`${コーヒーの種類}ができました!☕`);
resolve(`熱々の${コーヒーの種類}`);
}, 2000); // 2秒かかる
});
}
// 使ってみよう!
console.log("🏪 コーヒーショップへようこそ!");
コーヒーを注文する("エスプレッソ")
.then((完成したコーヒー) => {
console.log(`お客さん:${完成したコーヒー}をいただきました!😋`);
return コーヒーを注文する("カプチーノ"); // 次のコーヒーを注文
})
.then((次のコーヒー) => {
console.log(`お客さん:${次のコーヒー}も美味しい!🥰`);
})
.catch((エラー) => {
console.error(`問題発生!:${エラー.message} 😱`);
})
.finally(() => {
console.log("お会計はレジでお願いします 💰");
});
console.log("バリスタ:次のご注文どうぞ!"); // これが先に実行される!
🚀 第三世代:async/await(2017年〜)
—「魔法の待機」方式
// 最新のasync/await方式(裏はPromise)
async function 朝のルーティン() {
try {
console.log("🌞 おはようございます!朝の準備を始めます");
const 朝食 = await 朝食を作る(); // 完了するまで待機
console.log(`${朝食}を食べました!`);
const 歯 = await 歯を磨く(); // 完了するまで待機
console.log(`${歯}がピカピカになりました!`);
console.log("さあ、出勤しましょう!🚶♂️");
} catch (エラー) {
console.error(`問題発生!:${エラー.message} 😱`);
}
}
// これすごく読みやすい!👍
// シンプルな同期コードみたいに書けるけど、裏では非同期で動いている
🔍 実例:「推しの画像検索アプリ」を作ってみよう
ボタンをクリックすると、Unsplash APIから「猫」の画像をランダムで表示するアプリを考えてみましょう:
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>推し猫検索アプリ🐱</title>
<style>
body {
font-family: "Hiragino Kaku Gothic ProN", sans-serif;
text-align: center;
background-color: #f5f5f5;
padding: 20px;
}
.container {
max-width: 600px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
.cat-img {
max-width: 100%;
border-radius: 5px;
margin-top: 20px;
box-shadow: 0 0 5px rgba(0,0,0,0.2);
}
button {
background: #ff6b6b;
color: white;
border: none;
padding: 10px 20px;
font-size: 16px;
border-radius: 5px;
cursor: pointer;
transition: 0.3s;
}
button:hover {
background: #ff5252;
transform: translateY(-2px);
}
.loader {
border: 5px solid #f3f3f3;
border-top: 5px solid #ff6b6b;
border-radius: 50%;
width: 50px;
height: 50px;
animation: spin 1s linear infinite;
margin: 20px auto;
display: none;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error {
color: #ff5252;
font-weight: bold;
}
</style>
</head>
<body>
<div class="container">
<h1>🐱 推し猫検索アプリ 🐱</h1>
<p>ボタンを押して、かわいい猫ちゃんを召喚しよう!</p>
<button id="cat-button">にゃんこ召喚!</button>
<div id="loader" class="loader"></div>
<div id="error" class="error"></div>
<div id="cat-container"></div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const button = document.getElementById('cat-button');
const loader = document.getElementById('loader');
const errorDiv = document.getElementById('error');
const catContainer = document.getElementById('cat-container');
button.addEventListener('click', async () => {
try {
// リセット
catContainer.innerHTML = '';
errorDiv.textContent = '';
loader.style.display = 'block';
button.disabled = true;
// 🔍 猫の画像を取得(非同期処理)
const catData = await fetchRandomCat();
// 画像表示
const img = document.createElement('img');
img.src = catData.urls.regular;
img.alt = catData.alt_description || '猫の画像';
img.className = 'cat-img';
// ちょっとした演出(猫を待たせすぎないように!)
setTimeout(() => {
loader.style.display = 'none';
catContainer.appendChild(img);
button.disabled = false;
// 猫の鳴き声を追加(ちょっとしたおまけ)
const meows = ['にゃー!', 'みゃお〜', 'にゃん♪', 'ふにゃ〜'];
const randomMeow = meows[Math.floor(Math.random() * meows.length)];
const meowEl = document.createElement('p');
meowEl.textContent = randomMeow;
catContainer.appendChild(meowEl);
}, 500);
} catch (error) {
loader.style.display = 'none';
errorDiv.textContent = `😿 にゃんこ召喚に失敗: ${error.message}`;
button.disabled = false;
}
});
// 猫の画像を取得する関数
async function fetchRandomCat() {
// 注意: 実際のアプリではUnsplashのAPI keyが必要です
const response = await fetch('https://api.unsplash.com/photos/random?query=cat&client_id=YOUR_API_KEY');
if (!response.ok) {
throw new Error(`HTTPエラー: ${response.status}`);
}
return await response.json();
}
});
</script>
</body>
</html>
📋 非同期処理を使うときの7つのベストプラクティス
-
エラーハンドリングは必ず書こう
「エラーなんて起きないよ」と思った瞬間に限って、必ず何かが壊れます。try { const data = await 危険な関数(); } catch (error) { console.error('お詫び:想定外のニャンコ発生', error); // ユーザーにも優しく伝える }
-
Loading状態を表示しよう
ユーザーは「何が起きているのか」を知りたがっています。「処理中...」の表示があるだけで、イライラ度は激減します。 -
タイムアウトを設定しよう
永遠に待ち続けるのはやめましょう。// 10秒でタイムアウトする関数 function withTimeout(promiseFunc, timeoutMs = 10000) { return Promise.race([ promiseFunc(), new Promise((_, reject) => setTimeout(() => reject(new Error('タイムアウトしました😵')), timeoutMs) ) ]); }
-
Promise.all()で並列処理
独立した複数の処理は、同時に実行しましょう。// 朝の準備を並列でやっちゃう!(実際はできないけど...) async function 効率的な朝() { const [コーヒー, ニュース, 天気] = await Promise.all([ コーヒーを入れる(), ニュースを読む(), 天気を確認する() ]); console.log(`${天気}の朝に${コーヒー}を飲みながら${ニュース}を読んでいます`); }
-
キャンセル可能な非同期処理
最新のブラウザなら、AbortControllerが使えます。const controller = new AbortController(); const signal = controller.signal; // キャンセルボタンを押したとき cancelButton.addEventListener('click', () => { controller.abort(); // 処理をキャンセル! }); try { const response = await fetch('https://api.example.com/data', { signal }); const data = await response.json(); } catch (error) { if (error.name === 'AbortError') { console.log('ユーザーによってキャンセルされました'); } else { console.error('エラー発生:', error); } }
-
デバウンス/スロットリングを使う
検索入力欄などでは、ユーザーのタイピングごとにAPIリクエストを飛ばすのは非効率です。// 検索入力の200ms後にのみAPIを呼び出す function debounce(func, wait = 200) { let timeout; return function(...args) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); }; } const debouncedSearch = debounce((query) => { searchAPI(query); }); // 入力欄のイベントリスナー searchInput.addEventListener('input', (e) => { debouncedSearch(e.target.value); });
-
状態管理はきちんと設計
大規模なアプリでは、非同期処理の状態管理が重要です。// シンプルな状態管理の例 const state = { data: null, loading: false, error: null }; function updateState(newState) { Object.assign(state, newState); renderUI(); // UIの再描画 } async function fetchData() { updateState({ loading: true, error: null }); try { const data = await apiCall(); updateState({ data, loading: false }); } catch (error) { updateState({ error, loading: false }); } }
🧠 非同期マスターへの道:思考法
非同期処理をマスターするには、「料理人思考」が役立ちます。料理人は複数の料理を同時に進行させながら、それぞれの完成時間を考慮して効率よく仕事をします。
シェフの秘訣:
「肉を焼いている間にソースを作り、ソースを煮詰めている間に皿を温め...」
同様に、JavaScriptでも「この処理が完了するまでの間に他の何ができるか」と考えることで、効率的なコードが書けるようになります。
🎯 まとめ:非同期処理の進化
JavaScriptの非同期処理は、「コールバック → Promise → async/await」と進化してきました。進化の目的は常に「人間にとって読みやすく、書きやすいコード」です。
コールバック:「終わったら電話するね」
Promise:「ここに予約券があるよ。完成したらこれと交換してね」
async/await:「ちょっと待っててね(魔法の杖を振る)はい、できたよ!」
今日から非同期処理の考え方を意識して、スムーズに動作するWebアプリケーションを作ってみましょう!そして何より、await
を使うときは、コードを書いているあなた自身も「せっかちにならず」に待つことを忘れないでくださいね😉
🐾 おまけ:非同期処理あるある
-
「あれ?データが空...」:
await
を忘れたせいでPromiseオブジェクトをそのまま使おうとしてる -
「なんでエラーが出ないの...?」:
.catch()
を書き忘れたので、エラーが闇に消えている -
「この変数、なんで
undefined
なの...」: 非同期処理の外で結果を使おうとしている - 「何回もAPIが呼ばれるんだけど...」: デバウンス処理を忘れている
JavaScriptの非同期処理はときに不思議な動きをしますが、基本を理解すれば怖くありません。むしろ、「裏で勝手に働いてくれる小人たち」と思えば、とても便利な機能です!