JavaScript | DOM 操作:イベント発展 – イベント伝播(キャプチャ / バブル)

JavaScript JavaScript
スポンサーリンク

イベント伝播とは何か(キャプチャ/バブルの全体像)

イベント伝播は「ある要素で起きたイベントが、DOM ツリーの上下へ段階的に届く仕組み」です。段階は「キャプチャ(外側→内側)→ターゲット(実際の要素)→バブル(内側→外側)」の順。ここが重要です:リスナーをどの段階で受けるかを意識すると、意図通りの順序で処理でき、競合や二重反応を防げます。通常はバブルで受け、特殊要件だけキャプチャを使います。


3段階の流れをコードで体感する

キャプチャ/ターゲット/バブルのログ出力

<div id="outer" style="padding:10px;border:1px solid #ccc">
  <button id="inner">押す</button>
</div>
<script>
  outer.addEventListener("click", () => console.log("キャプチャ:外"), { capture: true });
  inner.addEventListener("click", () => console.log("ターゲット:内"));
  outer.addEventListener("click", () => console.log("バブル:外"));
</script>
HTML

ここが重要です:同じイベントでも、登録した段階によって“いつ実行されるか”が変わります。{ capture: true } でキャプチャ段階、未指定はバブル段階です。

currentTarget と target の違いを合わせて理解

<script>
  outer.addEventListener("click", (e) => {
    console.log("段階:キャプチャ  target:", e.target.id, "current:", e.currentTarget.id);
  }, { capture: true });

  inner.addEventListener("click", (e) => {
    console.log("段階:ターゲット target:", e.target.id, "current:", e.currentTarget.id);
  });

  outer.addEventListener("click", (e) => {
    console.log("段階:バブル    target:", e.target.id, "current:", e.currentTarget.id);
  });
</script>
HTML

ここが重要です:e.target は“発生源”、e.currentTarget は“今処理中の要素”。段階が違っても定義は同じです。


キャプチャを使う場面(先取りしたい特殊要件)

外側で“先に”拾って特別扱いする

<div id="layer" style="padding:10px;border:1px solid #ccc">
  <button id="btn">押す</button>
</div>
<script>
  layer.addEventListener("click", (e) => {
    // 先取りして全体ロジックを適用
    console.log("キャプチャ優先ロジック");
  }, { capture: true });

  btn.addEventListener("click", () => console.log("通常ロジック"));
</script>
HTML

ここが重要です:キャプチャは“内側に降りる前”に受けるため、優先ロジックや監査・ロギングなどで使われます。通常の UI 制御はバブルで十分です。

キャプチャで遮断(やむを得ないときだけ)

<script>
  document.addEventListener("click", (e) => {
    // 特定条件で内側へ降りない
    if (/* 条件 */) e.stopPropagation(); // キャプチャ段階で遮断
  }, { capture: true });
</script>
HTML

ここが重要です:キャプチャでの stopPropagation は“内側に到達させない”強力な手段。乱用すると予期せぬ機能停止を招くため、理由が明確なときだけ使います。


バブルを使う場面(委譲・規模・保守性)

親でまとめて受けるイベント委譲

<ul id="list"></ul>
<script>
  list.addEventListener("click", (e) => {
    const del = e.target.closest(".delete");
    if (del && list.contains(del)) { del.closest("li")?.remove(); return; }

    const edit = e.target.closest(".edit");
    if (edit && list.contains(edit)) { console.log("編集"); return; }
  });

  const li = document.createElement("li");
  li.innerHTML = `りんご <button class="delete">削除</button>`;
  list.appendChild(li);
</script>
HTML

ここが重要です:バブル段階で“親一箇所”にリスナーを付けると、後から追加される要素にも自動対応でき、リスナー数とコストを抑えられます。closest+contains で安全に対象を選別します。

外クリック検知(内側はバブルで遮断、外側で受ける)

<div id="menu" class="open">
  <button id="toggle">開閉</button>
</div>
<script>
  toggle.addEventListener("click", (e) => e.stopPropagation()); // 内側クリックは外に伝えない

  document.addEventListener("click", () => {
    menu.classList.remove("open"); // バブルで届く“外側クリック”だけ処理
  });
</script>
HTML

ここが重要です:内側で stopPropagation、外側でバブルを受けるのが定番の組み合わせ。誤閉じを防ぎつつ、直感的な挙動になります。


伝播制御の道具(stopPropagation/stopImmediatePropagation/preventDefault)

何を止めるかの違いを明確に

<button id="inner">押す</button>
<script>
  inner.addEventListener("click", (e) => {
    // e.preventDefault();        // 既定動作(遷移・送信など)を止める
    // e.stopPropagation();       // 親への伝播を止める(委譲のハンドラも止まる)
    // e.stopImmediatePropagation(); // 同じ要素の後続ハンドラも含め完全停止
  });
</script>
HTML

ここが重要です:伝播(外側へ届くか)と既定動作(ブラウザ標準の挙動)は別物。役割を混同しない。強力な stopImmediatePropagation は最小限に。


実務指針(どの段階で受けるべきか)

原則は“バブル”で設計する

  • 理由: 委譲が使え、後から増える要素にも強い。コードが短く、保守性が高い。
  • 例外: 監査・ロギング・グローバルな優先処理など、内側に届く前に扱いたい場合だけキャプチャ。

競合は“順序”と“範囲”で解く

  • 順序: キャプチャを使えば先に、バブルは後に動く。優先度設計を明確に。
  • 範囲: closest/contains/composedPath() で“意図した塊”だけを対象にし、不要な stopPropagation を減らす。

ここが重要です:まず“バブル+委譲”で設計し、どうしても先取りが必要な箇所のみ“キャプチャ”を採用する。小さな例外に留めると拡張が容易です。


落とし穴と回避策(デバッグしやすく、壊れにくく)

伝播停止の乱用で親の処理が死ぬ

無闇に stopPropagation を入れると、上位の正しいロジックが実行されず、拡張が難しくなります。必要な箇所に限定し、まずは条件分岐で選別する設計に。

preventDefault で伝播は止まらない

“既定動作を止める”だけでは親ハンドラは動きます。外へ届かせたくないときは stopPropagation を選ぶ。

複雑ツリーで target が想定外

SVG・シャドウDOM・疑似要素などで想定外の target になり得ます。composedPath() を確認し、closest と contains で堅牢に対象を絞る。


まとめ

イベント伝播は「キャプチャ → ターゲット → バブル」の三段階。通常はバブルで受け、委譲で規模に強い設計にする。先取りが必要な特殊要件のみキャプチャを使い、競合は段階(順序)と範囲(closest/contains/composedPath)で解決する。止めるべきは“伝播”なのか“既定動作”なのかを常に切り分け、stopPropagation/stopImmediatePropagation/preventDefault を正しく使い分ける。これを徹底すれば、初心者でも読みやすく壊れにくいイベント処理の設計ができます。

タイトルとURLをコピーしました