C
JavaScript/非同期/Lesson 15

イベントループ — *JS がシングルスレッドなのに速い理由*

45分·theory
このチャプター
1/3

イベントループ — *JS がシングルスレッドなのに速い理由*

🎯 このレッスンを読み終えたら

このレッスンを読み終えると、以下の3つを自信を持って扱えるようになります。

  • ✅ イベントキャプチャリング・バブリング・委譲 (delegation) パターン
  • addEventListener のクリーンアップ漏れによるメモリリーク
  • ✅ debounce / throttle を適用するタイミング

学習目標をチェックリストとして手元に置き、すべてに答えられるようになったらレッスンを閉じてください。

シングルスレッド + イベントループ

核心を一言で

JS は一度に一つだけ処理するシングルスレッド言語ですが、膨大な数のタスクを並行して処理できます。その秘密はイベントループ (Event Loop)非同期処理の外部委譲です。

シングルスレッド — 落とし穴?

javascript
console.log("1");
slowOperation();        // 5秒かかる
console.log("2");

slowOperation が5秒かかると、その間は何もできません。ユーザーのクリックも無視されます。これがブロッキングです。

解決策: ブロッキング処理を外部に委ねて、JS は次の処理を続けます。

イベントループの動作

code
┌──────────────────┐
│  Call Stack      │   ← 現在実行中の関数
│  (シングル実行)   │
└────────┬─────────┘
         │ 非同期処理が終わったら
         │
┌────────▼─────────┐
│   Task Queue     │   ← setTimeout・イベントなどの待機列
└────────┬─────────┘
         │ Stack が空になったら取り出す
         ▼
       Event Loop (常に巡回)

ブラウザ・Node.js が別スレッドで非同期処理 (タイマー・ネットワーク・ファイル) を実行します。完了するとQueue にコールバックを追加し、Stack が空になった時点で Event Loop が取り出して実行します。

最も有名なクイズ

javascript
console.log("1");
setTimeout(() => console.log("2"), 0);
console.log("3");

// 出力: 1, 3, 2

setTimeout(..., 0) なのに、なぜ 2最後に出力されるのか?

  • setTimeout のコールバックは Task Queue に入る
  • 現在のコード (console.log("3")) が終わったに処理される

setTimeout遅延時間 0即時ではなく、現在のコードが終わり次第できるだけ早くという意味です。

Microtask と Macrotask

Queue には2種類あります:

  • Microtask — Promise・queueMicrotask。優先度高
  • Macrotask — setTimeout・イベント・I/O。優先度低
javascript
console.log("1");
setTimeout(() => console.log("2"), 0);          // Macrotask
Promise.resolve().then(() => console.log("3"));  // Microtask
console.log("4");

// 出力: 1, 4, 3, 2

現在のコードが終わると:
1. Microtask をすべて処理 (3)
2. 次に Macrotask を一つ処理 (2)

非同期 = 外部に委ねること

javascript
// 同期 — JS が直接処理
const x = 5 + 3;

// 非同期 — 外部に委ねる
setTimeout(() => {}, 1000);       // ブラウザのタイマー
fetch('/api');                     // ブラウザのネットワーク
fs.readFile(...);                  // Node のファイルシステム

これらは JS ではなく環境 (ブラウザ・Node) が処理し、完了時にコールバックを Queue に追加します。JS 本体は待たずに次のコードへ進みます。

なぜ速いのか

1万人が同時アクセスしてもユーザーごとに別スレッドを作らず:

  • リクエスト 1 開始 → DB 照会を外部委譲 → 次のリクエストを処理
  • リクエスト 2 開始 → DB 照会を委譲 → さらに次のリクエストを処理
  • DB のレスポンスが返ったら → 各コールバックを処理

1スレッドで大量の並行処理。Node.js・Nginx が少ないメモリで大量トラフィックを捌ける秘訣です。

落とし穴 — CPU バウンドな処理

javascript
// ❌ 重い計算 — メインスレッドをブロック
for (let i = 0; i < 1e9; i++) {
    sum += i;
}
// この間、クリックもレンダリングもすべて停止

JS の非同期の強みI/O 処理にのみ有効です。CPU バウンドな処理は依然としてブロッキングです。

解決策:

  • 小さなチャンクに分けて setTimeout で処理
  • Web Worker (ブラウザ) / Worker Threads (Node) — 別スレッド

まとめ

  • JS = シングルスレッド + イベントループ
  • 非同期処理は外部に委譲
  • Microtask (Promise) > Macrotask (setTimeout)
  • I/O に強くCPU に弱い
  • CPU 処理は Worker に任せる

⚡ やってみよう — Microtask vs Macrotask の順序

下記コードの出力順序を予測してから実行してみましょう。Promise (microtask) > setTimeout (macrotask)。
✏️ JS 코드
📟 コンソール出力
▶ 実行ボタンを押してください
⚠️ ブラウザのサンドボックスで実行 — console.log()のみ対応、alert/fetchは不可

🤖 AI にこう依頼してみよう

このレッスンの概念を理解すれば、AI に具体的な指示が出せるようになります。漠然とした「直して」ではなく、語彙を持ったリクエスト — それがトークン節約の出発点です。

  • 「このクリックイベントに debounce 300ms を適用して」
  • 「このイベントハンドラーにクリーンアップ (return () => removeEventListener) も追加して」

なぜこれでトークンが節約できるのか

概念を知らないと、AI の回答を受け取っても「それって何ですか?」と再度聞くことになります。その「再質問」がトークンを消費します。概念を一度理解しておくと、会話が一回で完結します。

先に読むとよい概念: イベント処理
次のおすすめ: Promise
イベントループ - JavaScript