【JavaScript】Event Delegationで効率的にイベントを管理する方法|バブリング・closest・data属性・動的DOM対応

JavaScript

イベントを多数の要素に個別登録すると、メモリ消費や再描画コストが増え、動的に追加される要素には再バインドが必要になります。Event Delegation(イベント委譲)は、親要素に一度だけリスナーを置き、イベントのバブリングを利用して子要素の操作をまとめて扱う設計です。コードが簡潔になり、パフォーマンスと保守性が向上します。

この記事でわかること

  • バブリングを利用した委譲の仕組み(event.target / closest
  • 動的に増える要素を1つのリスナーで扱う基本パターン
  • data属性で複数アクションを振り分ける書き方
  • フォーム入力の委譲とキーボード操作(アクセシビリティ)
  • 委譲が向かないケースと対処(focusin / focusout
スポンサーリンク

Event Delegationの仕組みと前提

ブラウザのイベントは通常、発生源の要素から親方向へ伝播します。この性質をバブリングと呼びます。親に設置したリスナーは event.targetclosest を使って、どの子要素で起きたのかを識別できます。委譲はこの仕組みを利用して、親一箇所にだけリスナーを登録し、条件分岐で目的の子要素に対する処理を行います。

e.targete.currentTarget の違い
e.currentTarget はリスナーを持つを、e.target は実際に発火した子要素を指します。委譲ではこの違いを前提にロジックを書きます。クリックイベントやバブリングの基礎はクリックイベントの設定方法(バブリング・イベント委任)も参照してください。

基本パターン

ボタンが動的に増える場面でも、親にだけリスナーを置けば十分です。クリックされた要素から目的のセレクタを closest で探索し、該当したときだけ処理を実行します。

HTML + JavaScript:基本の委譲
<div id="list">
  <button class="item" data-id="1">編集</button>
  <button class="item" data-id="2">編集</button>
</div>

<script>
const list = document.getElementById('list');

list.addEventListener('click', (e) => {
  const btn = e.target.closest('.item');
  if (!btn || !list.contains(btn)) return;       // 委譲先のガード
  const id = btn.dataset.id;
  console.log('編集クリック:', id);
});
</script>
ガード(早期return)を先頭に置く
closestnull を返す(対象外をクリックした)場合や、list.contains(btn) で範囲外を弾くガードを先頭に書くと、以降のロジックが安全になりネストも浅く保てます。

複数アクションを一箇所で扱う

data属性に役割を持たせると、ひとつのリスナーで様々な操作を切り替えられます。条件分岐が増える場合はオブジェクトマップで処理を振り分けると読みやすくなります。

HTML + JavaScript:data-action で振り分け
<div id="cards">
  <button data-action="edit" data-id="10">編集</button>
  <button data-action="delete" data-id="10">削除</button>
</div>

<script>
const actions = {
  edit: (id) => console.log('編集', id),
  delete: (id) => console.log('削除', id)
};

document.getElementById('cards').addEventListener('click', (e) => {
  const el = e.target.closest('[data-action]');
  if (!el) return;
  const fn = actions[el.dataset.action];
  if (typeof fn === 'function') fn(el.dataset.id);
});
</script>

このマップ方式は分岐が増えても見通しが良く、拡張に強い書き方です。コールバックや関数の基礎は関数の基本と使い方も参考になります。

フォームや入力イベントの委譲

入力系は changeinput を親フォームで受けると、要素追加時の再バインドを避けられます。対象の nametype で分岐すれば、検証や同期処理を一括管理できます。

HTML + JavaScript:input の委譲とバリデーション
<form id="profile">
  <input type="text" name="username">
  <input type="email" name="email">
</form>

<script>
const form = document.getElementById('profile');

form.addEventListener('input', (e) => {
  const el = e.target;
  if (el.name === 'username') {
    // 例:リアルタイムバリデーション
    el.setCustomValidity(el.value.length < 3 ? '3文字以上入力してください' : '');
  }
  if (el.name === 'email') {
    // 例:簡易チェック
    el.setCustomValidity(el.validity.typeMismatch ? 'メール形式が不正です' : '');
  }
});
</script>

送信時のローディングや二重送信防止と組み合わせると、より実用的になります。フォーム送信のローディング実装も合わせてご覧ください。

イベント伝播と停止の取り扱い

stopPropagation の多用に注意
委譲では親でイベントを受けるため、子側で stopPropagation を多用すると親が拾えなくなります。必要最小限に留め、委譲設計を優先する場合は子の個別リスナーではなく、親で条件分岐する方針に統一します。

動的DOMの追加と削除に強い理由

委譲は親にだけリスナーが存在するため、子の追加や削除で再登録は不要です。仮想DOMやサーバーレンダリング後にフラグメントを差し込むケースでも、一度の初期化で継続的に機能します。マウスオーバーでの動的な要素追加・削除と委譲の組み合わせはマウスオーバーで要素を動的に追加・削除する実践ガイドで詳しく扱っています。

アクセシビリティとキーボード操作

クリックだけに頼らず、EnterSpace による操作も親でまとめて扱うと、ボタン以外の要素にも一貫した操作体験を提供できます。roletabindex を適切に付与し、keydown での分岐を追加します。

HTML + JavaScript:クリックとキーボードの両対応
<div id="menu">
  <div role="button" tabindex="0" data-action="open">開く</div>
</div>

<script>
const menu = document.getElementById('menu');

function handleAction(el) {
  if (!el) return;
  if (el.dataset.action === 'open') console.log('メニューを開く');
}

menu.addEventListener('click', (e) => handleAction(e.target.closest('[data-action]')));
menu.addEventListener('keydown', (e) => {
  if (e.key === 'Enter' || e.key === ' ') {
    const el = e.target.closest('[data-action]');
    if (el) { e.preventDefault(); handleAction(el); }
  }
});
</script>

委譲が向かないケースと対処

バブリングしないイベントや、focus のように挙動が特殊なイベントは委譲に向きません。

focus / blur は focusin / focusout で受ける
focusblurバブリングしないため、そのままでは親で委譲できません。バブリングする focusin / focusout を使えば親で受け取れます。また、親子階層が極端に複雑でヒットテストに時間がかかる場合は、セレクタを絞って closest の探索深度を抑え、必要なら要所だけ個別リスナーを併用します。

実運用でのベストプラクティス

  • 範囲は最小に:親が広すぎると余計なイベントも拾うため、機能単位で最小のラッパー要素を用意する
  • 判定は matches / closestdataset でアクションを表現すると拡張に強い
  • 早期returnを先頭に:例外処理や無関係領域の除外を先に置き、ネストを浅く保つ
  • テスト:動的追加・削除とキーボード操作の両方をカバーし、stopPropagation の影響を受けないことを確認する

よくある質問(FAQ)

QEvent Delegationとは何ですか?
A子要素のイベントを個別に登録せず、親要素でまとめて処理する手法です。event.target で実際にイベントが発生した要素を特定します。動的に追加される要素にも自動で対応できます。
QEvent Delegationを使うメリットは?
Aイベントリスナーの数を減らせるためメモリ効率が良くなります。また動的に追加された要素にも特別な処理なくイベントが機能します。大量のリスト項目などに特に有効です。
Qクリックされたのがどの子要素かを特定するには?
Aevent.target.closest(".item") で、対象セレクタに一番近い祖先(自身を含む)を取得できます。これでクリックが子の内部要素に当たっても、正しく .item を特定できます。
Qfocus や blur が親で拾えません。
Afocus / blurバブリングしないため委譲できません。バブリングする focusin / focusout を使えば親で受け取れます。

まとめ

Event Delegation はバブリングを活用して親に一度だけリスナーを置き、子要素のイベントを識別して処理する設計です。動的要素への自動対応・リスナー数の削減・保守性の向上といった利点があり、closestdataset を組み合わせると現場で扱いやすくなります。伝播や特殊イベントの性質を理解し、親のスコープ設計と早期returnのパターンを徹底することで、スケーラブルで読みやすいUIロジックを実現できます。