はじめに (対象読者・この記事でわかること)
この記事は、JavaScript の基礎は理解しているが、イベント委譲(Event Delegation)を実装したときに event 引数がどこから来るのか疑問に思った初心者〜中級者の方を対象としています。
読了後は、
1. event オブジェクトがブラウザのイベントループ内でどのように生成・伝搬されるか、
2. 委譲対象の要素で event を受け取る仕組みとそのプロパティの意味、
3. 実際のコードに落とし込み、デバッグ時に event が期待通りに動作しない原因を特定できるようになる、ことができるようになります。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。
- 基本的な HTML と CSS の知識
- JavaScript の変数・関数・DOM 操作(addEventListener 等)に慣れていること
- ブラウザのデベロッパーツールでコンソール出力ができること
イベント委譲とは?:概要と背景
イベント委譲は、多数の子要素に個別にリスナーを貼る代わりに、共通の親要素に一つだけリスナーを設定し、イベントがバブリング(伝搬)してくるのを利用するテクニックです。
DOM ツリーが大きくなるほど、個別にリスナーを付与するとメモリ使用量が増え、要素の追加・削除時にリスナー管理が煩雑になります。そこで、親要素が子要素からのイベントを捕捉し、event.target を使って実際に操作すべき要素を特定します。
しかし、event パラメータはどこから来るのか が不明確だと、event.target が null になったり、期待しない要素情報が入っていたりしてデバッグが困難です。実は、event はブラウザの イベントループ が生成し、リスナー関数が呼び出されるときに自動的に渡される オブジェクトです。以下でその流れを詳細に追います。
実装と仕組みの徹底解説:具体的な手順や実装方法
ステップ1 :基本的な委譲リスナーを作る
Html<ul id="menu"> <li data-action="new">新規作成</li> <li data-action="open">開く</li> <li data-action="save">保存</li> </ul> <script> const menu = document.getElementById('menu'); menu.addEventListener('click', function(event) { // ここで渡されてくる event が問題の中心 console.log(event); // ← ブラウザが自動で渡すオブジェクト const li = event.target; // クリックされた要素 if (li.tagName === 'LI') { console.log('action:', li.dataset.action); } }); </script>
ポイント
- addEventListener の第2引数は 関数 であり、ブラウザは内部で event オブジェクトを生成し、関数呼び出し時に第1引数として渡します。
- event は Event インターフェース(または派生クラス MouseEvent 等)を実装したオブジェクトで、type, target, currentTarget, bubbles, cancelable などのプロパティを持ちます。
ステップ2 :イベント生成の裏側を追う
- ユーザー操作(マウスクリック)が OS → ブラウザに届く。
- ブラウザは イベントキュー に
clickイベントオブジェクトをプッシュ。 - イベントループがキューを取り出し、対象ノード(クリック位置の要素)から上方向へ バブリング を開始。
- バブリング途中の各ノードに対して、登録されたリスナー関数 が順次呼び出される。呼び出し時に 同一の
eventオブジェクト が引数として渡される。 event.currentTargetは「現在処理中のリスナーが登録された要素」を示し、event.targetは「実際にクリックされた要素」を示す。
この流れが 「event がどこから来るか」 の答えです。開発者が意識すべきは「リスナーが呼び出された瞬間にブラウザが渡す」こと、そして 同一オブジェクトがバブリング全体で共有 される点です。
ステップ3 :実装上の落とし穴と対策
1. event が undefined になるケース
- 原因:リスナー関数を
addEventListenerに渡す際に()を付けてしまう(即時実行)
js // NG パターン menu.addEventListener('click', handleClick()); // handleClick の戻り値が渡される - 対策:関数リファレンスだけを渡すか、必要ならアロー関数でラップする。
js menu.addEventListener('click', handleClick); // 正しい // または menu.addEventListener('click', e => handleClick(e));
2. event.target が期待した要素でない
- 原因:子要素(例:
<span>)がクリックされた場合、targetはその子要素になる。 - 対策:
event.currentTargetと組み合わせて委譲対象の親要素を基準にし、closestで目的の要素を取得する。js menu.addEventListener('click', function(event) { const li = event.target.closest('li'); if (!li) return; // li 以外の領域クリックは無視 console.log('action:', li.dataset.action); });
3. stopPropagation が意図せずバブリングを止める
- 原因:子要素側のリスナーで
event.stopPropagation()を呼んでしまうと、親の委譲リスナーに届かなくなる。 - 対策:委譲を前提に設計する場合は、子要素側での停止処理を避けるか、捕捉 (capture) フェーズでリスナーを登録して回避できる。
js // capture フェーズで登録 menu.addEventListener('click', handler, true);
ハマった点やエラー解決
- 症状:
console.log(event)がnullのように見える。 - 原因:Chrome の DevTools で
console.log(event)を直接書くと、遅延評価のためコンソールに表示されたときにはオブジェクトが既にリセットされた状態になることがある。 -
解決策:
console.log(JSON.stringify(event, null, 2))でスナップショットを取るか、Object.assign({}, event)でコピーしてから出力する。 -
症状:委譲リスナーが全く反応しない。
- 原因:
addEventListenerの第3引数にfalse(デフォルト)を指定したが、CSS のpointer-events: none;が親要素に適用されていた。 - 解決策:
pointer-eventsをautoに戻す、もしくは子要素側でクリックハンドラを付与してバブリングを強制する。
まとめ
本記事では、イベント委譲で受け取る event オブジェクトがブラウザのイベントループによって生成され、バブリング途中で同一インスタンスがリスナーに自動渡しされる仕組み を解説しました。
- event は addEventListener が呼ばれた瞬間にブラウザが提供するオブジェクト。
- バブリング全体で同一オブジェクトが共有され、target と currentTarget の違いを正しく理解することが重要。
- 実装時の典型的なミス(即時実行、stopPropagation、pointer-events 等)とその対処法も紹介しました。
この知識を活かすことで、デバッグがスムーズになるだけでなく、イベント委譲を安全かつ効率的に使いこなすことができるようになります。次回は、「Capture フェーズを使った高度な委譲」や 「カスタムイベントの作成」 について掘り下げる予定です。
参考資料
- MDN Web Docs – Event
- MDN Web Docs – Event delegation
- 《JavaScript 本格入門》 第5章「DOM イベントとバブリング」
- You Might Not Need jQuery – Event Delegation