Knowledge‎ > ‎

キャプチャリングとバブリング


click, keydownに代表される、いわゆるDOMのイベントの「フェーズ」についての話です。

イベントのフェーズの種類

DOMのイベントには、キャプチャリングフェーズ・ターゲットフェーズ・バブリングフェーズの3つのフェーズがあります。それぞれ以下の特徴があります。

フェーズ イベントオブジェクト上の定数と値 備考
キャプチャリング CAPTURING_PHASE 1 Windowオブジェクトからイベントが発生した要素の親まで順に、イベント情報が送信されるフェーズです。ターゲットフェーズの前に発生します。
ターゲット AT_TARGET 2 イベントが発生した要素に、イベント情報が送信されるフェーズです。
バブリング BUBBLING_PHASE 3 イベントが発生した要素の親からWindowオブジェクトまで順に、イベント情報が送信されるフェーズです。ターゲットフェーズの後に発生します。ただし、バブリングしないイベントもあります。これについては後述します

eventPhaseプロパティや定数を使ったサンプル

<div id="target">[ここは処理しない]
	<div>[ここは処理する]</div>
	<div>[ここは処理する]</div>
</div>[ここは処理しない]

<script>
document.getElementById("target").addEventListener("click", function(e) {
	// if分の条件式の右辺は「e.BUBBLING_PHASE」では無く「3」と書いても動作は同じだが
	// 定数を使ったほうが意味合いがわかりやすくなり、また間違いも減る
	if (e.eventPhase == e.BUBBLING_PHASE) {
		alert("OK");
	}
}, false);
</script>
※サンプルでは、イベントフェーズを意識し定数を使用して対象要素を判別する例を示しました。しかし実際には、TABLE要素上でTD要素で発生したイベントを要素名から判別したり、class, name, idなどの属性から対象要素を判別するほうが多いと思います。

イベントリスナーを登録する方法

(IE8以前以外では)addEventListener関数でイベントを登録することができます。
target.addEventListener(type, listener, useCapture);
  • 第一引数には、「"click"」「"keydown"」などのイベントの種類を文字列で指定します。
  • 第二引数には、イベントが発生したときに実行する関数(コールバック関数)を指定します。これをイベントリスナー(もしくはイベントハンドラ)と呼びます。
  • 第三引数には、キャプチャリング(true)・バブリング(false)のどちらのフェーズに登録するかを指定します(省略可。省略時はfalse)。一般的に、どちらでもいい場合にはfalseを指定します(もしくは無指定)。あえて「キャプチャリングを使用したいとき」に、trueを指定します。イベントが発生する要素は必ずターゲットフェーズになるため第三引数の指定は無視されます。

IE8以前でも似た機能としてattachEventが用意されています。しかしそもそもIE8以前にはキャプチャリングフェーズがありません。そのため当然、フェーズの指定をすることはできません。

また、特にメソッドを使用せずとも、以下のように要素のイベントプロパティに直接設定してもイベントリスナーを登録することができます。
<input type="button" onclick="…">
elm.onclick = function() {…};
ただしこちらもフェーズの指定ができないため、やはりキャプチャリングフェーズで使用することはできません。


それぞれの設定方法についてまとめておきます
登録方法 キャプチャリングフェーズでの使用 複数のイベントリスナーの設定 対応ブラウザ
addEventListener 可能 可能 IE9以降, Chrome, FireFox, Safari, Operaなど
attachEvent 不可能 可能 IE, Opera
要素のイベントプロパティに設定 不可能 不可能 IE, Chrome, FireFox, Safari, Operaなど
※対応ブラウザのバージョンは2013/02時点の最新のものとする

では、「キャプチャリングを使用したいとき」とは、どんなケースなのでしょうか?
その前に親要素で処理を行なうメリットを説明します。

親要素で処理を行なうメリット

例えばTABLEのTD要素をクリックしたときの挙動を設定したいときに、すべてのTD要素にイベントリスナーを設定するのは手間ですし、その分メモリを消費する事になります。このような場合、親要素にイベントリスナーを登録すると1ヶ所に集約する事ができます。また親要素で登録しておけば、その子要素として対象の種類の要素が追加されたときでも、別途イベントを追加する必要がなくなります。

<table id="data">
	<tr><td>1<td>2
	<tr><td>3<td>4
</table>

<script>
var data = document.getElementsByTagName("TD");
// ループで4つの要素にそれぞれイベントリスナーを登録している。当然、その分のメモリを消費する
for (var i = 0; i < data.length; i++) {
	data[i].addEventListener("click", function(e) {alert(e.target.innerText);}, false);
}
</script>

<table id="data">
	<tr><td>1<td>2
	<tr><td>3<td>4
</table>

<script>
// 1つの要素にのみイベントリスナーを登録している。
document.getElementById("data").addEventListener("click", function(e) {
	if (e.target.tagName == "TD") {
		alert(e.target.innerText);
	}
}, false);
</script>

キャプチャリングを使用する必要があるケース

以下のようなケースでキャプチャリングを使用します。
  • バブリングフェーズがないイベントで、親要素で処理したい場合
  • ターゲットと(1つまたは複数の)親要素で2度以上イベントを発生させる場合でかつ、親要素のイベントを子要素に優先させて実行したいとき

(1)バブリングフェーズがないイベント

バブリングしないイベントを親要素で処理したいときにはキャプチャリングフェーズで処理します。バブリングしないイベントとして以下が挙げられます。
  • load / unload
  • mouseenter / mouseleave
  • focus / blur
バブリングするかどうかは、イベントオブジェクトのbubblesプロパティで示されています。値がtrueならそのイベントにはバブリングフェーズがあることを示しています。load / unloadの親要素での処理などは、有意義な用途が思いつきませんが、blur、つまりフォーカスがはずれたときに入力チェックを行うという使い方なら実用性があるでしょう。

<input type="text" name="…">
<input type="text" name="…">
<input type="text" name="…">

<script>
document.body.addEventListener("blur", function(e) {
	if (e.target.tagName == "INPUT" && e.target.type == "text") {
		if (e.target.value == "") {
			alert("何か入力してください");
		}
	}
}, true);
</script>

(2)親要素のイベントを子要素に優先させて実行したい

例えば発生したイベントを中断したい場合には、ターゲットフェーズの前、つまりキャプチャリングフェーズで処理を行なう必要があります。ただし必要性が発生するのは、以下に該当する場合など少し特殊な状況のみになると思います。
  • 共通関数で多くの要素にイベントが設定されている
  • 共通関数は変えられない
  • 特定のケースだけイベントを中断したい

イベントの中断

イベントは、「キャプチャ → ターゲット → パブリック」と順番に実行されますが、この流れを途中で終了したい場合は、 stopImmediatePropagation() メソッドか stopPropagation() メソッドを使用します。
stopImmediatePropagation() メソッドは直ちに中断したい場合に使用します。
stopPropagation() メソッドは現在のノードの処理が終わってから中断したい場合に使用します。



2013/05/01