DOMイベント:イベントオブジェクトとイベント伝播(キャプチャリング / バブリング)
1) イベントオブジェクト(Event)の要点
Event)の要点イベントハンドラに渡される引数(慣例的に event / e)は、発生したイベントの詳細を表すオブジェクトです。主なプロパティ・メソッドは次のとおりです。
代表的プロパティ
-
type:イベント種別(例:"click"、"input") -
target:実際にイベントが発生した最下位の要素(※しばしば子要素) -
currentTarget:現在ハンドラが実行されている要素(委譲時に重要) -
eventPhase:どのフェーズで実行中か
1 = CAPTURING_PHASE/2 = AT_TARGET/3 = BUBBLING_PHASE -
bubbles:バブリングするか(true/false) -
cancelable:preventDefault()可能か(true/false) -
defaultPrevented:既にpreventDefault()済みか -
timeStamp:発生時刻(数値) -
isTrusted:ユーザ操作によるものか(true)/ スクリプト発火か(false) -
composed:Shadow DOM の境界をまたぐか -
detail:一部の UI イベントで追加情報(例:dblclickではクリック回数等) -
ポインタ・マウス・キーボード系のサブクラス
MouseEvent(clientX/Yなど)、PointerEvent(ポインタID、圧力など)、KeyboardEvent(key、code、ctrlKeyなど)
代表的メソッド
-
preventDefault():ブラウザの既定動作を抑止(リンク遷移・フォーム送信など)。cancelableがtrueのときのみ有効。 -
stopPropagation():これ以降の 上位要素 への伝播(キャプチャ/バブル)を停止。 -
stopImmediatePropagation():同一要素上の 残りのハンドラ 実行も含めて即時停止。 -
composedPath():イベントが通過する実際のノード配列(Shadow DOM を含む経路)
補足:
event.targetとevent.currentTargetは混同しがちです。委譲(親にハンドラを付けて子のイベントを拾う)では常にcurrentTargetを信頼し、targetは「どの子が起点か」を特定するためにclosest()などと併用します。
2) イベント伝播の仕組み(3フェーズ)
-
キャプチャリング(上から下へ):
window → document → html → body → ... → 目標要素
addEventListener(type, handler, { capture: true })でこのフェーズにハンドラを登録。 -
ターゲット(AT_TARGET):目標要素上で発火。
-
バブリング(下から上へ):
目標要素 → 親 → ... → body → html → document → window
既定はバブリングフェーズでの実行(capture: false)。
同じ要素に キャプチャとバブルの両方 があれば、キャプチャが先、次いでターゲット、最後にバブルの順で呼ばれます。
伝播を止める・既定動作を止める
-
伝播停止:
stopPropagation()/ さらに強力:stopImmediatePropagation() -
既定動作停止:
preventDefault()(フォーム送信、リンク遷移、コンテキストメニューなど) -
passive: trueのハンドラではpreventDefault()は無効(警告が出る/無視される)。主にスクロール系のパフォーマンス最適化に使われます。
3) 基本コード例(キャプチャ vs バブルの順序を可視化)
#inner ボタンをクリックすると、概ね次の順序で出力されます。
[capture] outer → [capture] middle → [capture] inner → [bubble ] inner → [bubble ] middle → [bubble ] outer
4) 伝播制御の違い(停止の粒度)
5) イベント委譲(バブリングを活用した定番パターン)
多量の子要素に個別ハンドラを付けず、親に 1 つだけ付けます。動的追加要素にも有効です。
6) addEventListener のオプションと伝播への影響
addEventListener のオプションと伝播への影響-
{ capture: true }:キャプチャフェーズで実行 -
{ once: true }:一度だけ実行後に自動解除(伝播そのものは通常どおり) -
{ passive: true }:スクロール/タッチ等でpreventDefault()無効化(フレーム落ち防止) -
{ signal: controller.signal }:AbortControllerでまとめて解除可能(クリーンアップに有用)
7) よくある非バブリング/注意が必要なイベント
-
バブリングしない:
mouseenter/mouseleave、pointerenter/pointerleave、load、unload、resize(window)、scroll(多くの実装で要素上は非バブリング)など
→ 代替としてmouseover/mouseout、pointerover/pointeroutを使うと委譲しやすい。 -
フォーカス関連:
focus/blurは従来非バブリング。委譲したい場合はfocusin/focusout(バブリングする)を使用。 -
Shadow DOM:
composed: trueのイベント(例:click)は影DOM境界を越えられる。composedPath()で正確な経路を取得可能。
8) デバッグのコツ
-
ログに
eventPhase・event.target・event.currentTargetを必ず出す。 -
まず全てバブリングで組み、必要最小限だけ
capture:trueを使う。 -
ライブラリ混在時は
stopPropagation()の乱用を避ける。想定外のハンドラが呼ばれなくなる原因になりやすい。 -
パフォーマンス重視のスクロール・タッチは
passive:trueを検討し、preventDefault()が必要な場合は設計を見直す。
9) まとめ
-
イベントは キャプチャ → ターゲット → バブル の順で伝播します。
-
委譲 はバブリングを活用する代表的手法で、動的UIに強い。
-
制御は目的別に使い分ける:
既定動作抑止は preventDefault()、伝播停止は stopPropagation()、同一要素の他ハンドラも止めるなら stopImmediatePropagation()。 -
target と currentTarget の役割を理解し、composedPath() や closest() を併用して堅牢なハンドラを書く。
ChatGPT5 生成日:2025/09/11