イベントは一つの要素のみで発生しているわけではありません。
イベントが発生する時、きっかけとなる要素から他の要素にも伝搬していくような仕組みになっています。
今回は、イベント伝搬のキーワードとなるバブリングとキャプチャリングの仕組みについて解説していきます。
イベント伝搬
イベント伝搬とは、ある要素からイベントが伝わり広がっていくことです。
イベントがどこかの要素で発生すると、その要素から他の要素へとイベントが伝搬していきます。
イベント伝搬は、以下の3つのフェーズの道筋を通ります。
- キャプチャリングフェーズ:Windowオブジェクトから子の要素に下りていくフェーズ
- ターゲットフェーズ:イベントが発生した要素に到達するフェーズ
- バブリングフェーズ:イベントが発生した要素から親要素に上っていくフェーズ
例えば、以下のHTMLドキュメントがあるとします。
<html>
<head></head>
<body>
<h1>イベントの伝搬</h1>
<div>
<input id="elem" type="button" value="button">
</div>
</body>
</html>
このHTMLページにある”button”がクリックされた時、イベントはこのような流れで伝搬していきます。
input
をクリックした場合、イベントは一番トップにあるWindow
から下りていき(キャプチャリング)、ターゲットであるinput
に到達します。
到達後、ハンドラを呼び出しながらWindow
を目掛けて上っていきます(バブリング)。
フェーズごとに詳しく見ていきましょう。
バブリング
バブリングはイベント伝搬としては最後のフェーズですが、特に重要なフェーズのため先に解説をします。
バブリングは、ある要素のイベントが発生すると、はじめにその要素のハンドラが実行され、次に親要素、さらにその上の親要素へイベントが伝搬されていきます。
そのため、ターゲット要素の祖先である要素もバブリングフェーズによってイベントが発生し、登録されているハンドラが呼び出されます。
例えば、以下のようにsection > div > p
それぞれの要素にハンドラが登録されているとします。
<style>
body {
width: 740px;
}
section,section div,p {
border: 2px solid blue;
margin: 5px;
padding: 5px;
}
</style>
<h1>バブリング</h1>
<section onclick="console.log('section')">section
<div onclick="console.log('div')">div
<p onclick="console.log('p')">p</p>
</div>
</section>
<p>
をクリックしてみます。
すると、各要素に登録されているハンドラが呼び出され、p
→ div
→ section
の順番で出力されていることが分かります。
実際にクリックされた<p>
なら分かりますが、なぜ<div>
と<section>
までハンドラが実行されたのでしょうか?
それは、<p>
で発生したクリックイベントが<div>
に渡り、その後<section>
、そしてdocument
まで渡っているからです。
では、次に<div>
をクリックしてみます。
今度はイベントが発生した<div>
から<section>
に渡るため、出力されるのはdiv
→ section
になります。
このように、イベント発生源の要素から上に向かってイベントが水の泡のように伝わっていくことからバブリングと表現されます。
ターゲット
キャプチャリングフェーズの後、実際にイベントが発生した要素にイベントが到達した段階のことをターゲットフェーズと呼びます。
実際にイベントが発生した要素は、もっとも階層が深い位置にいます。この要素のことをターゲット要素と呼びます。
ターゲット要素は、event.target
でアクセスすることが可能です。
<style>
body {
width: 740px;
}
section,section div,p {
border: 2px solid blue;
margin: 5px;
padding: 5px;
}
</style>
<h1>ターゲット</h1>
<section>section
<div>div
<p>p</p>
</div>
</section>
<script>
document.onclick = function(event) {
// ターゲット要素のタグ名を出力
console.log(event.target.tagName);
};
</script>
<div>
をクリックすると、このように”DIV”のみが出力されるはずです。
注意する点は、ターゲットフェーズは単独で処理される訳ではないことです。
キャプチャリングフェーズとバブリングフェースの一連の流れにターゲットフェーズが存在するため、あえてイベントフェーズという表現をしないことも多いです。
キャプチャリング
イベント発生後、Document
オブジェクトからターゲット要素まで下へ向かってイベントが流れてくるフェーズをキャプチャリングフェーズと呼びます。
先にバブリングを説明した理由は、実はキャプチャリングがほとんど使われないためです。
キャプチャリングフェーズでイベントが発生しても、イベントハンドラやイベントリスナーを使って登録されたハンドラは、イベントが起きたことを知らないため実行されません。通常は、ターゲットフェーズとバブリングフェーズのみで実行されます。
一方で、明示的にキャプチャリングフェーズでイベントをキャッチすることも可能です。
その場合、addEventListener
メソッドの三つ目の引数にtrue
を指定します。
// どちらでも可
EventTarget.addEventListener(event, handler, true);
EventTarget.addEventListener(event, handler, {capture: true});
三つ目の引数は省略可能で、何も指定しない場合デフォルトでfalse
が設定されます。
そのため、キャプチャリングフェーズでハンドラを実行したい場合、明示的に設定する必要があるということです。
キャプチャリングとバブリングの両方の例
最後に、キャプチャリングフェーズとバブリングフェーズの両方でハンドラが実行された場合の例を見てみましょう。
以下は、ドキュメント上のいずれかの要素をクリックすると、キャプチャリングとバブリングの両フェーズでハンドラが実行されるコードです。
<style>
body {
width: 740px;
}
section,section div,p {
border: 2px solid blue;
margin: 5px;
padding: 5px;
}
</style>
<h1>キャプチャリングからバブリング</h1>
<section>section
<div>div
<p>p</p>
</div>
</section>
<script>
for (let elem of document.querySelectorAll('*')) {
// キャプチャリングフェーズ
elem.addEventListener('click', () => console.log(`キャプチャリング: ${elem.tagName}`), true);
// バブリングフェーズ
elem.addEventListener('click', () => console.log(`バブリング: ${elem.tagName}`));
};
</script>
試しに<p>
をクリックしてみます。
P
が2回出力されています。
これは、キャンプチャリングとバブリング両方でハンドラをセットしているためです。
このような流れから、キャプチャリングフェーズからターゲットフェーズまでの伝搬と、ターゲットフェーズからバブリングフェーズまでのイベント伝搬があることが分かります。
まとめ
今回は、バブリングとキャプチャリングについて解説しました。
// ポイント
* イベントは、以下の3つのフェーズを通って伝搬されていく
1. キャプチャリングフェーズ:Windowオブジェクトから子の要素に下りていくフェーズ
2. ターゲットフェーズ:イベントが発生した要素に到達するフェーズ
3. バブリングフェーズ:イベントが発生した要素から親要素に上っていくフェーズ
イベント伝搬は、以降の記事で学ぶさまざまなイベント処理のパターンで重要な基盤となります。しっかり頭に入れておきましょう。
合わせて読みたいイベント概要シリーズ
第1回:イベントハンドラ
第2回:イベントリスナー
第3回:イベントオブジェクト
第4回:バブリングとキャプチャリング(当記事)
第5回:Event.targetとEvent.currentTarget
第6回:イベント移譲
第7回:デフォルト動作のキャンセル
第8回:新しいイベントの発生
コメント