JavaScript や jQuery を使ったプログラミングで、DOM イベントのバブリングという言葉を耳にします。本などを読んでもピンと来なかったので、理解するためにサンプルを作って動かしてみました。あまり面白くないかもしれませんが、せっかく作ったので書いておきます。
上の画像のサンプルは、実際に動かして試すことができるよう 実験室 にアップしました。興味のある方は試してみてください。ソースコードはこの記事の下の方に記載しています。
DOM イベントは、イベントの原因となったオブジェクトで発生するだけでなく、キャプチャリングとバブリングというイベントの伝播があります。ここが .NET Framework のイベントとは異なっていて、自分が理解に苦しんだところです。
どこかの DOM オブジェクトでイベントが発生すると、window オブジェクト(ブラウザによっては document オブジェクト)とイベントが起きたオブジェクトの間を、DOM ツリーの親子関係を順にたぐって、イベントが伝播していきます。
伝播は 3 つのフェーズに分かれており、Capturing Phase(捕捉フェーズ)⇒ Target Phase(対象フェーズ)⇒ Bubbling Phase(浮上フェーズ)という順番になります。それぞれのフェーズの説明は以下の通りです。
-
Capturing Phase では、window ⇒ document ⇒ その中の親 ⇒ 子 ⇒ 孫 ⇒ ひ孫 ・・・といった具合に、親子関係を親側から順にたぐって、イベントが起きたオブジェクトの親まで(「親まで」という点に注意)の各オブジェクトにイベントが送信されていきます。
-
Target Phase では、イベントの原因となったオブジェクトにイベントが送信されます。
-
Bubbling Phase では、イベントを発生させたオブジェクトの親から(「親から」という点に注意)順に浮上していき、window に達するまで各オブジェクトに順にイベントが送信されていきます。
window からイベントを発生させたオブジェクトの間の、親子関係にある任意のオブジェクトにリスナーを登録しておけば、そのオブジェクトにイベントが送信されたタイミングで必要な処置ができます。
オブジェクトにリスナーを��録する方法は、(1) 当該オブジェクトの addEventListener メソッド を呼び出す、(2) HTML 要素の属性を利用する、(3) 当該オブジェクトのプロパティを利用する、の 3 つがあります。
注意しなければならないのは、上記の (1) 以外の方法では、Capturing Phase でイベントを捕捉できないことです。
さらに注意しなければならないのは、IE8 以前では addEventListener メソッドはサポートされていないことです。代わりに attachEvent メソッドが使えますが、これは Capturing Phase でイベントを捕捉できません。(attachEvent メソッドの詳細は後述します)
IE9 は DOM Level 3 Events をサポートしているそうなので(詳しくは MSDN の IEBlog DOM Level 3 Events support in IE9 を参照)、addEventListener メソッド を使用可能です。
addEventListener メソッドを利用してリスナーを登録し、Capturing Phase、Target Phase、Bubbling Phase でイベントを補足するサンプルを書いてみました。そのソースコードをアップしておきます。
なお、Target Phase では、addEventListener メソッドで第 3 引数 useCapture を false に指定して登録したリスナーのみが呼び出されることになっているそうですが、自分が試した限りでは、useCapture を true に指定して登録したリスナーまでもが呼び出されてしまいました。
検証に使ったブラウザは、IE9、Firefox 17.0, Chrome 23.0.1271.95 m, Opera 12.02, Safari 5.1.7 で、すべて同じ結果になりました。
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Event Capturing and Bubbling</title>
<style type="text/css">
#div1 { border: 1px solid black; width: 200px;
height: 100px; }
#table1 { border: 1px solid red; width: 150px; }
td { border: 1px solid green; }
#span1 { background-color: yellow; }
</style>
<script type="text/javascript">
//<![CDATA[
// IE9, Mozzilla 用リスナー。
function listener1(event) {
// イベントが発生すると event オブジェクトが生成
// され、その参照が第一引数に渡される。それから
// target プロパティでイベントを発生させたオブジ
// ェクトを、eventPhase プロパティでフェーズ情
// 報を取得できる。this はアタッチしたオブジェク
// トへの参照となる。
var e = document.getElementById("result");
var phase = "";
if (event.eventPhase === 1) {
phase = "capturing phase";
} else if (event.eventPhase === 2) {
phase = "target phase";
} else if (event.eventPhase === 3) {
phase = "bubbling phase";
}
var str = "fired by: " + event.target.id +
", listened at: " + this.id +
", during " + phase + "<br />";
e.innerHTML += str;
}
// IE6-8 用リスナー。
function listener3(element) {
// attachEvent を使うと、this が参照するオブジェ
// クトは window になってしまい、アタッチしたオ
// ブジェクトへの参照は取得できない。止むを得な
// いので、オブジェクトへの参照は引数として渡す。
// Mozilla 系ではリスナーの第一引数には event オ
// ブジェクトへの参照が渡されるので注意。
var e = document.getElementById("result");
var str =
"fired by: " + window.event.srcElement.id +
", listened at: " + element.id + "<br />";
e.innerHTML += str;
}
// Bubbling Phase のリスナーをアタッチ。
function attachForBubbling(element) {
var e = document.getElementById("result");
if (element.addEventListener) {
// Bubbling Phase のリスナーをアタッチするに
// は第三引数を false に設定する。
element.addEventListener('click', listener1,
false);
e.innerHTML += "listener1 attached to " +
element.id +
" by addEventListener" +
" w/ useCapture=false<br />";
} else if (element.attachEvent) {
// アタッチするオブジェクトへの参照をリスナ
// ーの引数に渡すため、以下のように匿名関数
// を使う。ただし、匿名関数を使うとデタッチ
// できなくなることに注意。
element.attachEvent('onclick',
function () { listener3(element) });
e.innerHTML += "listener3 attached to " +
element.id + " by attachEvent<br />";
}
}
// Capturing Phase のリスナーをアタッチ。
// (IE9, Mozilla のみ)
function attachForCapturing(element) {
var e = document.getElementById("result");
if (element.addEventListener) {
// Capturing Phase 用リスナーをアタッチするに
// は第三引数を true に設定する。
element.addEventListener('click', listener1,
true);
e.innerHTML += "listener1 attached to " +
element.id +
" by addEventListener" +
" w/ useCapture=true<br />";
}
}
// リスナーを各オブジェクトにアタッチ。
window.onload = function () {
var element = document.getElementById("div1");
attachForBubbling(element);
attachForCapturing(element);
element = document.getElementById("table1");
attachForBubbling(element);
attachForCapturing(element);
element = document.getElementById("td1");
attachForBubbling(element);
attachForCapturing(element);
element = document.getElementById("span1");
attachForBubbling(element);
attachForCapturing(element);
}
//]]>
</script>
</head>
<body>
<div id="div1">
<table id="table1">
<tr>
<td id="td1">
<span id="span1">span1</span>
</td>
</tr>
<tr>
<td id="td2">
<span id="span2">span2</span>
</td>
</tr>
</table>
</div>
<input type="button" value="Clear Results"
onclick="javascript:result.innerHTML = '';"/>
<br />
<div id="result"></div>
</body>
</html>
上でも述べましたように、IE6-8 では、addEventListener メソッドはサポートされていませんが、代わりに
attachEvent メソッド がリスナーを登録するのに使えます。
IE6-8 をサポートするためには以下のようにします。上のサンプルコードでもこのようにして IE6-8 でリスナーを登録しています。
if (element.addEventListener) {
element.addEventListener('click', listener, false);
} else if (element.attachEvent){
element.attachEvent('onclick', listener);
}
attachEvent メソッドには以下のデメリットがあるので注意してください。
-
リスナー内の this で取得できるのが、window オブジェクトへの参照になる。(addEventListener メソッドの場合はリスナーをアタッチしたオブジェクトへの参照になる)
-
Capturing Phase ではイベントを補足できない。(Bubbling Phase と Target Phase では補足可能)
上記 1 の問題に対応するため、サンプルコードでは、リスナーの引数にアタッチするオブジェクトへの参照を渡しています。さらに、attachEvent メソッドの引数に匿名関数を使って、リスナーを登録しています。