Webページのボタンをクリックした時、実はそのイベントは一箇所だけで処理されているわけではありません。
ページ全体を通して「伝播」しているんです。
この記事では、Web開発で重要なイベントプロパゲーション(Event Propagation)について、初心者の方にも分かりやすく解説していきます。JavaScriptでイベント処理を扱う上で、必ず理解しておきたい基礎知識ですよ!
イベントプロパゲーションとは?
イベントプロパゲーション(Event Propagation)は、日本語で「イベントの伝播」という意味です。
ユーザーがWebページ上でクリックやキー入力などの操作をした時、そのイベントがDOM(Document Object Model)ツリーを通じて伝わっていく仕組みのことを指します。
簡単な例で理解しよう
HTMLでこんな構造があるとします:
<div id="parent">
親要素
<button id="child">ボタン</button>
</div>
ボタンをクリックした時、実は以下の要素すべてが「クリックされた」と認識されます:
- ボタン要素(直接クリックされた場所)
- 親のdiv要素
- body要素
- html要素
- document
なぜこんなことが起きるのでしょうか?それがイベントプロパゲーションの仕組みなんです。
イベントの伝播フロー:3つのフェーズ
イベントは、3つの段階を経て伝播します。
全体の流れ
1. キャプチャリングフェーズ(Capturing Phase)
↓
2. ターゲットフェーズ(Target Phase)
↓
3. バブリングフェーズ(Bubbling Phase)
それぞれを詳しく見ていきましょう。
フェーズ1:キャプチャリングフェーズ
ページの一番外側から、クリックされた要素に向かって降りていく段階です。
document
↓
html
↓
body
↓
div (親要素)
↓
button (ターゲット)
イベントがトップダウンで伝わっていきます。
フェーズ2:ターゲットフェーズ
実際にクリックされた要素に到達した段階です。
この時点で、その要素に設定されたイベントハンドラが実行されます。
フェーズ3:バブリングフェーズ
クリックされた要素から、ページの外側に向かって戻っていく段階です。
button (ターゲット)
↑
div (親要素)
↑
body
↑
html
↑
document
水の中の泡(バブル)が浮き上がっていくイメージから、「バブリング」と呼ばれています。
イベントバブリングの実例
最もよく遭遇するのが、このバブリングフェーズです。
コード例:バブリングを見てみよう
<div id="outer" style="padding: 50px; background: lightblue;">
外側のdiv
<div id="inner" style="padding: 30px; background: lightgreen;">
内側のdiv
<button id="btn">クリックしてね</button>
</div>
</div>
<script>
document.getElementById('outer').addEventListener('click', function() {
console.log('外側のdivがクリックされました');
});
document.getElementById('inner').addEventListener('click', function() {
console.log('内側のdivがクリックされました');
});
document.getElementById('btn').addEventListener('click', function() {
console.log('ボタンがクリックされました');
});
</script>
ボタンをクリックすると、コンソールに表示される順番:
ボタンがクリックされました
内側のdivがクリックされました
外側のdivがクリックされました
ボタンだけをクリックしたつもりでも、親要素のイベントハンドラも次々に実行されるんです。これがバブリングです。
イベントキャプチャリングの実例
キャプチャリングフェーズでイベントを捕まえることもできます。
addEventListener の第3引数
addEventListener
の第3引数にtrue
を指定すると、キャプチャリングフェーズでイベントを処理できます。
// 第3引数をtrueにするとキャプチャリング
element.addEventListener('click', handler, true);
// 第3引数を省略またはfalseにするとバブリング(デフォルト)
element.addEventListener('click', handler, false);
コード例:キャプチャリングとバブリングの違い
<div id="parent">
<button id="child">ボタン</button>
</div>
<script>
// キャプチャリングフェーズで捕捉
document.getElementById('parent').addEventListener('click', function() {
console.log('親要素(キャプチャリング)');
}, true);
// バブリングフェーズで捕捉
document.getElementById('parent').addEventListener('click', function() {
console.log('親要素(バブリング)');
}, false);
// ターゲット要素
document.getElementById('child').addEventListener('click', function() {
console.log('ボタン');
});
</script>
ボタンをクリックすると:
親要素(キャプチャリング) ← 先に実行される
ボタン
親要素(バブリング)
キャプチャリングが先、バブリングが後という順番が確認できます。
stopPropagation():伝播を止める
イベントの伝播を止めたい場合、stopPropagation()
メソッドを使います。
基本的な使い方
element.addEventListener('click', function(event) {
console.log('この要素で処理します');
// ここで伝播を止める
event.stopPropagation();
// 親要素には伝わらない
});
コード例:伝播を止める
<div id="parent" style="padding: 50px; background: lightblue;">
親要素
<button id="child">クリックしてね</button>
</div>
<script>
document.getElementById('parent').addEventListener('click', function() {
console.log('親要素がクリックされました');
});
document.getElementById('child').addEventListener('click', function(event) {
console.log('ボタンがクリックされました');
// ここで伝播を止める
event.stopPropagation();
});
</script>
結果:
ボタンをクリックしても、親要素のイベントハンドラは実行されません。
ボタンがクリックされました
伝播が止まったので、親要素には届かないんですね。
preventDefault()との違い
よく混同されるメソッドにpreventDefault()
があります。
それぞれの役割
stopPropagation():
- イベントの伝播を止める
- 親要素への伝播を防ぐ
preventDefault():
- イベントのデフォルト動作を止める
- 例:リンクのジャンプ、フォームの送信など
コード例:両者の違い
<a href="https://example.com" id="link">リンク</a>
<script>
document.getElementById('link').addEventListener('click', function(event) {
// デフォルト動作(ページ遷移)を止める
event.preventDefault();
console.log('ページは遷移しません');
});
</script>
使い分け:
- リンクのジャンプを止めたい →
preventDefault()
- 親要素への伝播を止めたい →
stopPropagation()
- 両方必要なら、両方呼ぶ
element.addEventListener('click', function(event) {
event.preventDefault(); // デフォルト動作を止める
event.stopPropagation(); // 伝播も止める
});
event.target と event.currentTarget
イベントオブジェクトには、2つの重要なプロパティがあります。
それぞれの意味
event.target:
- 実際にクリックされた要素
- 常にイベントが発生した最初の要素を指す
event.currentTarget:
- 現在イベントハンドラが設定されている要素
- イベントが伝播していく中で変化する
コード例:targetとcurrentTargetの違い
<div id="parent">
親要素
<button id="child">ボタン</button>
</div>
<script>
document.getElementById('parent').addEventListener('click', function(event) {
console.log('target:', event.target.id); // "child"
console.log('currentTarget:', event.currentTarget.id); // "parent"
});
</script>
ボタンをクリックした場合:
event.target
→ ボタン要素(実際にクリックされた場所)event.currentTarget
→ 親div要素(ハンドラが設定されている要素)
イベント委譲(Event Delegation)
イベントプロパゲーションを活用した、効率的なテクニックがあります。
問題:たくさんの要素にイベントを設定したい
<ul id="list">
<li>項目1</li>
<li>項目2</li>
<li>項目3</li>
<!-- ... 100個のli要素 ... -->
</ul>
すべての<li>
要素にイベントハンドラを設定するのは、非効率です。
解決策:親要素に1つだけ設定
バブリングを利用して、親要素で一括処理します。
// 非効率な方法(各li要素にイベントを設定)
document.querySelectorAll('li').forEach(function(li) {
li.addEventListener('click', function() {
console.log('クリックされました');
});
});
// 効率的な方法(親要素に1つだけ設定)
document.getElementById('list').addEventListener('click', function(event) {
// クリックされた要素がli要素かチェック
if (event.target.tagName === 'LI') {
console.log('クリックされました:', event.target.textContent);
}
});
メリット:
- イベントハンドラが1つだけで済む
- メモリ使用量が少ない
- 動的に追加された要素にも自動的に対応
コード例:動的に追加される要素にも対応
<ul id="list"></ul>
<button id="addBtn">項目を追加</button>
<script>
// 親要素にイベント委譲
document.getElementById('list').addEventListener('click', function(event) {
if (event.target.tagName === 'LI') {
alert('クリックされました: ' + event.target.textContent);
}
});
// 動的に項目を追加
let count = 0;
document.getElementById('addBtn').addEventListener('click', function() {
count++;
const li = document.createElement('li');
li.textContent = '項目' + count;
document.getElementById('list').appendChild(li);
});
</script>
後から追加された<li>
要素も、自動的にクリックイベントが動作します。
実際の使用例
例1:モーダルウィンドウの実装
モーダルの外側をクリックしたら閉じる、という動作を実装します。
<div id="modal" style="display:none; position:fixed; background:rgba(0,0,0,0.5);">
<div id="modalContent" style="background:white; padding:20px;">
<h2>モーダル</h2>
<p>コンテンツ</p>
<button id="closeBtn">閉じる</button>
</div>
</div>
<script>
const modal = document.getElementById('modal');
const modalContent = document.getElementById('modalContent');
// モーダル背景をクリックしたら閉じる
modal.addEventListener('click', function() {
modal.style.display = 'none';
});
// モーダル内容をクリックしても閉じない
modalContent.addEventListener('click', function(event) {
// 親要素(背景)への伝播を止める
event.stopPropagation();
});
// 閉じるボタン
document.getElementById('closeBtn').addEventListener('click', function() {
modal.style.display = 'none';
});
</script>
例2:ドロップダウンメニュー
メニュー外をクリックしたら閉じる機能を実装します。
<div id="dropdown">
<button id="menuBtn">メニュー</button>
<div id="menuContent" style="display:none;">
<a href="#">項目1</a>
<a href="#">項目2</a>
<a href="#">項目3</a>
</div>
</div>
<script>
const menuBtn = document.getElementById('menuBtn');
const menuContent = document.getElementById('menuContent');
// メニューボタンをクリック
menuBtn.addEventListener('click', function(event) {
menuContent.style.display =
menuContent.style.display === 'none' ? 'block' : 'none';
// documentへの伝播を止める(でないと下のハンドラが即座に実行される)
event.stopPropagation();
});
// document全体をクリックしたらメニューを閉じる
document.addEventListener('click', function() {
menuContent.style.display = 'none';
});
</script>
例3:テーブルの行クリック処理
表の行をクリックできるようにしつつ、ボタンのクリックとは区別します。
<table>
<tr class="row">
<td>商品A</td>
<td>1,000円</td>
<td><button class="deleteBtn">削除</button></td>
</tr>
<tr class="row">
<td>商品B</td>
<td>2,000円</td>
<td><button class="deleteBtn">削除</button></td>
</tr>
</table>
<script>
// 行全体のクリック
document.querySelectorAll('.row').forEach(function(row) {
row.addEventListener('click', function() {
alert('行がクリックされました');
});
});
// 削除ボタンのクリック
document.querySelectorAll('.deleteBtn').forEach(function(btn) {
btn.addEventListener('click', function(event) {
// 行のクリックイベントには伝播させない
event.stopPropagation();
alert('削除ボタンがクリックされました');
});
});
</script>
よくある問題と解決策
問題1:クリックしてもイベントが発火しない
原因:
親要素でstopPropagation()
が呼ばれている可能性があります。
解決策:
- イベントキャプチャリングを使う
- または、親要素の
stopPropagation()
を見直す
問題2:一度だけ実行したいのに何度も実行される
原因:
バブリングで複数の要素のハンドラが実行されています。
解決策:
element.addEventListener('click', function(event) {
// 伝播を止める
event.stopPropagation();
});
// または、once オプションを使う
element.addEventListener('click', handler, { once: true });
問題3:動的に追加した要素にイベントが効かない
原因:
要素追加前にイベントを設定しています。
解決策:
イベント委譲を使いましょう。
// NG:要素追加前に設定
document.querySelectorAll('.item').forEach(item => {
item.addEventListener('click', handler);
});
// OK:親要素にイベント委譲
document.getElementById('parent').addEventListener('click', function(event) {
if (event.target.classList.contains('item')) {
handler(event);
}
});
よくある疑問に答えます
Q. バブリングとキャプチャリング、どちらを使うべき?
ほとんどの場合、バブリング(デフォルト)で十分です。
キャプチャリングは特殊なケースでのみ使います。例えば、子要素のイベントより先に処理したい場合などです。
Q. stopPropagation()は使わない方がいい?
むやみに使うのは避けましょう。
stopPropagation()
を多用すると、他の部分のコードに影響を与える可能性があります。本当に必要な場合のみ使用し、イベント委譲などの別の方法も検討してください。
Q. 全てのイベントがバブリングする?
いいえ、一部のイベントはバブリングしません。
バブリングしないイベントの例:
focus
(代わりにfocusin
を使う)blur
(代わりにfocusout
を使う)load
scroll
(バブリングしない場合が多い)
Q. イベントプロパゲーションのパフォーマンス影響は?
通常は気にする必要はありません。
ただし、数百個の要素に個別にイベントハンドラを設定するより、イベント委譲を使った方がメモリ効率は良くなります。
まとめ:イベントの旅路を理解しよう
イベントプロパゲーションは、DOMツリーを通じてイベントが伝わる仕組みです。
重要ポイントをおさらい:
- イベントは3つのフェーズで伝播する(キャプチャリング→ターゲット→バブリング)
- デフォルトはバブリングフェーズで処理される
stopPropagation()
で伝播を止められるpreventDefault()
はデフォルト動作を止める(伝播とは別)event.target
は実際にクリックされた要素event.currentTarget
はハンドラが設定されている要素- イベント委譲でパフォーマンスを改善できる
- モーダルやドロップダウンメニューなど、実践的な使い道が多い
JavaScriptでインタラクティブなWebページを作る上で、イベントプロパゲーションの理解は欠かせません。
この仕組みを理解すれば、より柔軟で効率的なイベント処理が書けるようになりますよ!
コメント