TL;DR
- DOM のイベントは キャプチャ → ターゲット → バブリング の3フェーズを必ず通る
addEventListenerで指定するのは 「どのフェーズで handler を呼ぶか」- デフォルトは バブリングフェーズ
はじめに
JavaScriptを使っていると、
- なぜ親のイベントが反応するのか
- キャプチャとバブリングはどう使い分けるのか
といった疑問に一度はぶつかります。
この記事では、DOM イベント伝搬の仕組みを理解することを目標にします。
イベント伝搬の3フェーズ
DOM のイベントは、必ず次の3フェーズを通ります。
- キャプチャフェーズ
- ターゲットフェーズ
- バブリングフェーズ
これは仕様で決まっており、Reactなどのフレームワークには依存しません。
伝搬の流れ
たとえば、以下のような DOM 構造で <button> をクリックした場合:
<div id="outer">
<div id="inner">
<button id="target">クリック</button>
</div>
</div>
イベントは次のように伝搬します:
📍 1. キャプチャフェーズ(上から下へ)
↓
document
↓
#outer
↓
#inner
↓
📍 2. ターゲットフェーズ
↓
#target ← クリックされた要素
↓
📍 3. バブリングフェーズ(下から上へ)
↓
#inner
↓
#outer
↓
document
この一連の流れの中で、各要素に登録されたイベントハンドラが呼ばれます。
キャプチャフェーズ(Capturing phase)
由来
capture = 捕まえる
挙動
document→ 親 → 子 へ降りていきます- 「これから起きるイベント」を先回りして処理します
設定方法
element.addEventListener('click', handler, { capture: true });
ターゲットフェーズ(Target phase)
- 実際にイベントが発生した要素
- 専用の登録方法はありません
- ターゲット要素に登録された handler が実行される瞬間を指します
target.addEventListener('click', handler);
バブリングフェーズ(Bubbling phase)
由来
bubble = 泡
→ 下から上に浮かび上がるイメージ
挙動
- 子 → 親 → document へ伝わります
- 「起きた結果」に親が反応します
設定方法(デフォルト)
element.addEventListener('click', handler);
// または
element.addEventListener('click', handler, { capture: false });
適している用途の違い
キャプチャが適している用途(例外)
- モーダルの外側クリック検知
- グローバルなキーボードショートカット
- 操作ロック・ガード処理
- アナリティクス・監査ログ
バブリングが適している用途(デフォルト)
- 通常の UI 操作
- ボタン・リンクのクリック
- イベント委譲
event.target と event.currentTarget の違い
| プロパティ | 意味 |
|---|---|
| event.target | 実際にイベントが発生した要素 |
| event.currentTarget | 今 handler が実行されている要素 |
例
list.addEventListener('click', (e) => {
console.log(e.target); // li
console.log(e.currentTarget); // ul
});
targetは 発生源currentTargetは 責務の主体
フェーズが進むにつれて currentTarget は変わりますが、
target は常に同じです。
おわりに
これまで雰囲気でaddEventListenerなどを使っていましたが、イベント伝搬の仕組みを理解することができました。