DOMイベント:イベントオブジェクトとイベント伝播(キャプチャリング / バブリング)

1) イベントオブジェクト(Event)の要点

イベントハンドラに渡される引数(慣例的に event / e)は、発生したイベントの詳細を表すオブジェクトです。主なプロパティ・メソッドは次のとおりです。

代表的プロパティ

  • type:イベント種別(例:"click""input"

  • target:実際にイベントが発生した最下位の要素(※しばしば子要素)

  • currentTarget:現在ハンドラが実行されている要素(委譲時に重要)

  • eventPhase:どのフェーズで実行中か
    1 = CAPTURING_PHASE / 2 = AT_TARGET / 3 = BUBBLING_PHASE

  • bubbles:バブリングするか(true/false

  • cancelablepreventDefault() 可能か(true/false

  • defaultPrevented:既に preventDefault() 済みか

  • timeStamp:発生時刻(数値)

  • isTrusted:ユーザ操作によるものか(true)/ スクリプト発火か(false

  • composed:Shadow DOM の境界をまたぐか

  • detail:一部の UI イベントで追加情報(例:dblclick ではクリック回数等)

  • ポインタ・マウス・キーボード系のサブクラス
    MouseEventclientX/Y など)、PointerEvent(ポインタID、圧力など)、KeyboardEventkeycodectrlKey など)

代表的メソッド

  • preventDefault():ブラウザの既定動作を抑止(リンク遷移・フォーム送信など)。cancelabletrue のときのみ有効。

  • stopPropagation():これ以降の 上位要素 への伝播(キャプチャ/バブル)を停止。

  • stopImmediatePropagation():同一要素上の 残りのハンドラ 実行も含めて即時停止。

  • composedPath():イベントが通過する実際のノード配列(Shadow DOM を含む経路)

補足:event.targetevent.currentTarget は混同しがちです。委譲(親にハンドラを付けて子のイベントを拾う)では常に currentTarget を信頼し、target は「どの子が起点か」を特定するために closest() などと併用します。


2) イベント伝播の仕組み(3フェーズ)

  1. キャプチャリング(上から下へ):window → document → html → body → ... → 目標要素
    addEventListener(type, handler, { capture: true }) でこのフェーズにハンドラを登録。

  2. ターゲット(AT_TARGET):目標要素上で発火。

  3. バブリング(下から上へ):目標要素 → 親 → ... → body → html → document → window
    既定はバブリングフェーズでの実行(capture: false)。

同じ要素に キャプチャとバブルの両方 があれば、キャプチャが先、次いでターゲット、最後にバブルの順で呼ばれます。

伝播を止める・既定動作を止める

  • 伝播停止:stopPropagation() / さらに強力:stopImmediatePropagation()

  • 既定動作停止:preventDefault()(フォーム送信、リンク遷移、コンテキストメニューなど)

  • passive: true のハンドラでは preventDefault() は無効(警告が出る/無視される)。主にスクロール系のパフォーマンス最適化に使われます。


3) 基本コード例(キャプチャ vs バブルの順序を可視化)

html
<div id="outer"> <div id="middle"> <button id="inner">Click</button> </div> </div> <script> const log = (msg) => console.log(msg); const ids = ["outer","middle","inner"]; for (const id of ids) { const el = document.getElementById(id); // キャプチャ el.addEventListener("click", e => { log(`[capture] ${id} phase=${e.eventPhase}`); }, { capture: true }); // バブル(既定) el.addEventListener("click", e => { log(`[bubble ] ${id} phase=${e.eventPhase}`); }); } </script>

#inner ボタンをクリックすると、概ね次の順序で出力されます。
[capture] outer → [capture] middle → [capture] inner → [bubble ] inner → [bubble ] middle → [bubble ] outer


4) 伝播制御の違い(停止の粒度)

js
inner.addEventListener("click", e => { // 既定動作だけ止める(フォーカス移動やフォーム送信など) if (e.cancelable) e.preventDefault(); // 上位への伝播を止める(middle/outerへ上がらない) e.stopPropagation(); }); inner.addEventListener("click", e => { // こちらは呼ばれる(同一要素・同一フェーズ内の別ハンドラ) console.log("同一要素の別ハンドラ 1"); }); inner.addEventListener("click", e => { // 残り全部を止めたい場合はこちら e.stopImmediatePropagation(); console.log("この行以降、同一要素の他ハンドラは実行されない"); });

5) イベント委譲(バブリングを活用した定番パターン)

多量の子要素に個別ハンドラを付けず、親に 1 つだけ付けます。動的追加要素にも有効です。

html
<ul id="list"> <li data-id="1"><button>編集</button></li> <li data-id="2"><button>編集</button></li> </ul> <script> const list = document.getElementById("list"); list.addEventListener("click", (e) => { const li = e.target.closest("li[data-id]"); if (!li || !list.contains(li)) return; console.log("クリックされた行ID:", li.dataset.id); }); </script>

6) addEventListener のオプションと伝播への影響

  • { capture: true }:キャプチャフェーズで実行

  • { once: true }:一度だけ実行後に自動解除(伝播そのものは通常どおり)

  • { passive: true }:スクロール/タッチ等で preventDefault() 無効化(フレーム落ち防止)

  • { signal: controller.signal }AbortController でまとめて解除可能(クリーンアップに有用)

js
const controller = new AbortController(); window.addEventListener("scroll", onScroll, { passive: true, signal: controller.signal }); // 後で controller.abort() で一括解除

7) よくある非バブリング/注意が必要なイベント

  • バブリングしないmouseenter / mouseleavepointerenter / pointerleaveloadunloadresizewindow)、scroll(多くの実装で要素上は非バブリング)など
    → 代替として mouseover / mouseoutpointerover / pointerout を使うと委譲しやすい。

  • フォーカス関連focus / blur は従来非バブリング。委譲したい場合は focusin / focusout(バブリングする)を使用。

  • Shadow DOMcomposed: true のイベント(例:click)は影DOM境界を越えられる。composedPath() で正確な経路を取得可能。


8) デバッグのコツ

  • ログに eventPhaseevent.targetevent.currentTarget を必ず出す。

  • まず全てバブリングで組み、必要最小限だけ capture:true を使う。

  • ライブラリ混在時は stopPropagation() の乱用を避ける。想定外のハンドラが呼ばれなくなる原因になりやすい。

  • パフォーマンス重視のスクロール・タッチは passive:true を検討し、preventDefault() が必要な場合は設計を見直す。


9) まとめ

  • イベントは キャプチャ → ターゲット → バブル の順で伝播します。

  • 委譲 はバブリングを活用する代表的手法で、動的UIに強い。

  • 制御は目的別に使い分ける:
    既定動作抑止は preventDefault()、伝播停止は stopPropagation()、同一要素の他ハンドラも止めるなら stopImmediatePropagation()。

  • target と currentTarget の役割を理解し、composedPath() や closest() を併用して堅牢なハンドラを書く。

ChatGPT5 生成日:2025/09/11