JavaScript | DOM 操作:イベント発展 – イベント委譲

JavaScript
スポンサーリンク

イベント委譲とは何か

イベント委譲は、「たくさんの子要素それぞれにリスナーを付けず、親に1つだけ付けて子の操作に反応する」設計です。イベントがバブル(内側→外側)で伝わる仕組みを利用し、親のリスナー内で e.target と closest を使って“誰が操作されたか”を判定します。ここが重要です:委譲にすると、動的に追加された要素にも自動で対応でき、リスナーの数が増えず、性能と保守性が大幅に向上します。


なぜ委譲を使うのか(性能・動的要素・保守性)

規模に強い理由

  • 少ないリスナー: 親1個で済むため、メモリ消費・登録コストが小さい。
  • 動的追加に自動対応: 後から追加した子も、バブルで親に届くためそのまま動く。
  • 保守性の高さ: 付け外しの管理が“一箇所”になり、二重登録や外し忘れが減る。

典型の悪い例と改善

  • 悪い例: 1000件の行それぞれに click を付ける。スクロールや再描画で重くなる。
  • 改善: UL に click を1つだけ付け、e.target.closest(‘button.delete’) で削除ボタンを判定して処理。

ここが重要です:イベントの数ではなく“伝播の仕組み”を使って集約する。これが委譲の本質です。


基本の書き方(closest と contains の型)

親で子の操作を選別する

<ul id="list"></ul>
<script>
  const list = document.getElementById("list");

  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> <button class="edit">編集</button>`;
  list.appendChild(li);
</script>
HTML

ここが重要です:判定は target+closest、範囲チェックに currentTarget.contains(…)。この二つで“意図した塊だけ”を安全に扱えます。

よく使う補助

  • target と currentTarget:
    基準: 判定は e.target(発生源)、範囲の安全確認は e.currentTarget.contains(…)(親が所有)。
  • stopPropagation を極力使わない:
    理由: 親で選別すれば不要。遮断は“どうしても衝突する箇所のみ”に限定する。

UI別の委譲パターン(リスト、タブ、メニュー)

リスト行の削除・編集

<ul id="items"></ul>
<script>
  items.addEventListener("click", (e) => {
    const btn = e.target.closest("button");
    if (!btn || !items.contains(btn)) return;

    const row = btn.closest("li");
    if (btn.classList.contains("delete")) row?.remove();
    else if (btn.classList.contains("edit")) console.log("編集:", row?.textContent);
  });
</script>
HTML

ここが重要です:ボタン種別は class で分岐。closest で“ボタン → 行”へ辿るのが安定の型です。

タブ切り替え

<div id="tabs">
  <button data-tab="a">A</button>
  <button data-tab="b">B</button>
</div>
<div id="panel-a">A パネル</div>
<div id="panel-b" hidden>B パネル</div>
<script>
  tabs.addEventListener("click", (e) => {
    const btn = e.target.closest("#tabs [data-tab]");
    if (!btn) return;
    const tab = btn.dataset.tab;
    document.querySelector("#panel-a").hidden = tab !== "a";
    document.querySelector("#panel-b").hidden = tab !== "b";
  });
</script>
HTML

ここが重要です:親1箇所で“選択されたタブ”を判断し、表示の同期をまとめる。タブ追加にも強い。

メニューの外クリックで閉じる(委譲+範囲判定)

<div id="menu" class="open">…</div>
<script>
  document.addEventListener("click", (e) => {
    if (!menu.classList.contains("open")) return;
    if (!menu.contains(e.target)) menu.classList.remove("open");
  });

  // メニュー内クリックは普通に動かす(原則 stopPropagation なしで設計)
</script>
HTML

ここが重要です:contains で“外側クリック”だけを拾う。先に範囲判定できるなら stopPropagation は不要です。


高度なヒント(composedPath・キャプチャ・アクセシビリティ)

composedPath で経路を確認(シャドウDOM対策)

document.addEventListener("click", (e) => {
  const path = e.composedPath(); // 実際に通過した要素配列
  // path を使って、影響範囲や想定外のターゲットをデバッグ
});
JavaScript

ここが重要です:SVG・シャドウDOM・疑似要素で target が想定外になる場面の診断に有効。

どうしても先に処理したいときはキャプチャ

document.addEventListener("click", (e) => {
  // 監査やグローバル優先処理
}, { capture: true });
JavaScript

ここが重要です:通常の UI はバブルで受け、監査・ロギングなど“先取りしたい”例外だけキャプチャ。強い手段は例外的に。

アクセシビリティを優先する構造

  • ボタンは <button>:
    理由: Enter/Space の既定動作が生き、委譲と両立しやすい。
  • フォーカス設計:
    指針: キーボード操作も委譲のロジックで同じ分岐に乗せる(keydown で data-* を使った同等判定など)。

落とし穴と回避策(乱用を避け、デバッグしやすく)

乱用する stopPropagation で親の正しい処理が死ぬ

  • 対策: 親で“選別”する委譲を基本にし、衝突箇所だけに限定して遮断。

セレクタの曖昧さで誤判定

  • 対策: closest に“明確なセレクタ”を渡す。範囲は currentTarget.contains で保証する。

二重登録・外し忘れ

  • 対策: 親1箇所に集約。ページのマウント/アンマウントで付け外しをまとめる(AbortController で一括解除も可)。

重い処理を逐次で実行

  • 対策: ライブ更新は軽く、重い処理はデバウンスや確定タイミング(change/submit)へ寄せる。

まとめ

イベント委譲は、親1箇所にリスナーを置いて“子の操作を選別”する設計です。判定は e.target+closest、範囲保証に currentTarget.contains を使い、stopPropagation は原則不要(衝突箇所だけに限定)。通常はバブルで受け、例外のみキャプチャ。動的追加に強く、リスナーを減らせるため性能・保守性が高い。composedPath で複雑な経路も診断し、アクセシブルな要素選びで“止めなくてよい”UIを作る。これを徹底すれば、初心者でも規模に強く、読みやすく壊れにくいイベント処理を実装できます。

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