“`html
イベントを多数の要素に付与するとき、個々にリスナーを登録するとメモリ消費や再描画コストが増え、動的に追加される要素には再度バインドが必要になります。Event Delegation(イベント委譲)を使えば、親要素に一度だけリスナーを置き、イベントのバブリングを利用して子要素の操作をまとめて扱えるため、コードが簡潔になりパフォーマンスや保守性が向上します。
Event Delegationの仕組みと前提
ブラウザのイベントは通常、発生源の要素から親方向へ伝播します。この性質をバブリングと呼び、親に設置したリスナーはevent.targetやclosestを使って、どの子要素で起きたのかを識別できます。委譲はこの仕組みを利用して、親一箇所にだけリスナーを登録し、条件分岐で目的の子要素に対する処理を行います。
基本パターン
ボタンが動的に増える場面でも親にだけリスナーを置けば十分です。クリックされた要素から目的のセレクタをclosestで探索し、該当したときだけ処理を実行します。
<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>
複数アクションを一箇所で扱う
data属性に役割を持たせると、ひとつのリスナーで様々な操作を切り替えられます。条件分岐が増える場合はマップで処理を振り分けると読みやすくなります。
<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>
フォームや入力イベントの委譲
入力系はchangeやinputを親フォームで受けると、要素追加時の再バインドを避けられます。対象のnameやtypeで分岐すれば、検証や同期処理を一括管理できます。
<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を多用すると親が拾えなくなります。必要最小限に留め、委譲設計を優先する場合は子の個別リスナーではなく、親で条件分岐する方針に統一します。e.currentTargetはリスナーを持つ親を、e.targetは実際に発火した要素を指すため、両者の違いを前提にロジックを記述します。
動的DOMの追加と削除に強い理由
委譲は親にだけリスナーが存在するため、子の追加や削除で再登録は不要です。仮想DOMやサーバーレンダリング後にフラグメントを差し込むケースでも、一度の初期化で継続的に機能します。
アクセシビリティとキーボード操作
クリックだけに頼らず、EnterやSpaceによる操作も親でまとめて扱うと、ボタン以外の要素にも一貫した操作体験を提供できます。roleやtabindexを適切に付与し、keydownでの分岐を追加します。
<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を用いると親で受け取れます。親子階層が極端に複雑でヒットテストに時間がかかる場合は、セレクタを絞りclosestの探索深度を抑え、必要であれば要所だけ個別リスナーを併用します。
実運用でのベストプラクティス
親要素は範囲が広すぎると余計なイベントも拾うため、機能単位で最小のラッパー要素を用意します。判定はmatchesやclosestを使い、datasetでアクションを表現すると拡張に強くなります。例外処理や無関係領域の早期returnを先頭に置き、ネストを浅くして読みやすさを維持します。テストでは動的追加・削除とキーボード操作の両方をカバーし、stopPropagationの影響を受けないことを確認します。
まとめ
Event Delegationはバブリングを活用して親に一度だけリスナーを置き、子要素のイベントを識別して処理する設計です。動的要素への自動対応、リスナー数の削減、保守性の向上といった利点があり、closestとdatasetを組み合わせると現場で扱いやすくなります。伝播や特殊イベントの性質を理解し、親のスコープ設計と早期returnのパターンを徹底することで、スケーラブルで読みやすいUIロジックを実現できます。
“`