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

サイ本の17.2に詳しく書いてありますが、JavaScriptのDOM レベル2イベントでは、
イベント伝播は3つの段階で構成されます。

  1. キャプチャリングフェーズ
  2. ターゲットノード自身でのイベントハンドラの実行
  3. バブリングフェーズ


上図のように3つ入れ子になった要素の、
外側から2つ目の要素をクリックした場合を例にとると、
キャプチャリングフェーズでdocument→外側の要素→と伝播していき、
ターゲットノードでのイベントハンドラ(あれば)が実行され、
バブリングフェーズで→外側の要素→documentと伝播していきます。

DOMレベル0のイベントではターゲットノードでしか
イベントハンドラの実行は出来ませんが、
DOMレベル2イベントではキャプチャリングフェーズ・バブリングフェーズでも
イベントハンドラの実行が出来ます。

検証用HTML

確認のためにdivの親子をそれぞれクリックして、
イベントのlogを吐かせるだけの簡単なHTML+JavaScriptを用意しました。
このHTMLをhtmlファイルにして開くときは、必ずFirefoxで開いてください。
でないと、説明どおりの動作はしません。

<html>
<head>
<script>
(function(func){
	var temp = [];
	
	log = function(s){
		temp.push(s);
	};
	function init(){
			if(typeof document.readyState != 'undefined' && document.readyState != 'loaded' && document.readyState != 'complete'){
				setTimeout(arguments.callee,100);
				return;
			}
			var div = document.createElement('div');
			div.id = 'log';
			for(var i = 0;i < temp.length;i++){
				div.appendChild( document.createTextNode(temp[i]) );
				div.appendChild( document.createElement('br') );
			}
			
			document.body.appendChild(div);

			log = function(s){
				div.appendChild( document.createTextNode(s) );
				div.appendChild( document.createElement('br') );
			};
			func();
	}
	if(typeof document.readyState == 'undefined' && document.addEventListener){
		document.addEventListener('DOMContentLoaded',init, false );
	}else{
		init();
	}
})(function(){
	/* ロード時に実行されるコード */
	document.getElementById('outer1').addEventListener('click',function(e){eventLog(e);},true);
	document.getElementById('inner1').addEventListener('click',function(e){eventLog(e);},true);
	
	document.getElementById('outer2').addEventListener('click',function(e){eventLog(e);},false);
	document.getElementById('inner2').addEventListener('click',function(e){eventLog(e);},false);
	
	document.getElementById('outer3').addEventListener('click',function(e){eventLog(e);},true);
	document.getElementById('inner3').addEventListener('click',function(e){eventLog(e);},true);
	document.getElementById('outer3').addEventListener('click',function(e){eventLog(e);},false);
	document.getElementById('inner3').addEventListener('click',function(e){eventLog(e);},false);
	
	document.getElementById('outer4').addEventListener('click',function(e){eventLog(e);e.stopPropagation();},true);
	document.getElementById('inner4').addEventListener('click',function(e){eventLog(e);},true);
	
	document.getElementById('outer5').addEventListener('click',function(e){eventLog(e);},false);
	document.getElementById('inner5').addEventListener('click',function(e){eventLog(e);e.stopPropagation();},false);
	
	document.getElementById('hoge').addEventListener('click',function(e){eventLog(e);},false);
	document.getElementById('outer6').addEventListener('click',function(e){eventLog(e);e.stopPropagation();},false);
	document.getElementById('inner6').addEventListener('click',function(e){eventLog(e);},false);
});

function eventLog(e){
	log([
			['eventPhase',e.eventPhase].join(':'),
			['target',e.target['id']].join(':'),
			['currentTarget',e.currentTarget['id']].join(':')
		].join());
}
</script>
<style>
.outer{
	border:solid;
	width:40px;
	height:40px;
}
.inner{
	border:solid;
	width:20px;
	height:20px;
}
</style>
</head>
<body>
(1)内外共にuseCaptureにtrueを設定
<div id="outer1" class="outer">
	<div id="inner1" class="inner">
	</div>
</div>
(2)内外共にuseCaptureにfalseを設定
<div id="outer2" class="outer">
	<div id="inner2" class="inner">
	</div>
</div>
(3)内外共に、useCaptureにtrue・false両方でイベントハンドラを設定
<div id="outer3" class="outer">
	<div id="inner3" class="inner">
	</div>
</div>
(4)内外共にuseCaptureにtrueを設定、外側でEvent.stopPropagation()を実行する
<div id="outer4" class="outer">
	<div id="inner4" class="inner">
	</div>
</div>
(5)内外共にuseCaptureにfalseを設定、内側でEvent.stopPropagation()を実行する
<div id="outer5" class="outer">
	<div id="inner5" class="inner">
	</div>
</div>
(6)3つすべてでuseCaptureにfalseを設定、外側から2番目でEvent.stopPropagation()を実行する
<div id="hoge" style="border:solid;width:80px;height:80px;">
	<div id="outer6" class="outer">
		<div id="inner6" class="inner">
		</div>
	</div>
</div>
</body>
</html>

ログの見方は以下の通りです。

eventPhase:${イベントフェーズ},target:${ターゲットノードのid},currentTarget:${現在イベントが処理されているノードのid}
注:イベントフェーズは、1・キャプチャリング、2・ターゲットノード、3・バブリング

イベントハンドラ設定用のメソッドは以下のようになっていて、
useCaptureにtrueを設定するとキャプチャリングフェーズにイベントハンドラが実行、
falseを設定するとバブリングフェーズにイベントハンドラが実行されます。

addEventListener(type,listener,useCapture)
(1)内外共にuseCaptureにtrueを設定

この場合ターゲットノードの場合とキャプチャリングフェーズにイベントハンドラが実行されます。
外側のdiv(id=outer1)をクリックすると、以下のように表示されます。

eventPhase:2,target:outer1,currentTarget:outer1

内側のdiv(id=inner1)をクリックした場合は、以下のように表示されます。

eventPhase:1,target:inner1,currentTarget:outer1
eventPhase:2,target:inner1,currentTarget:inner1

外側、内側という順にイベントハンドラが実行されていること、
外側のdivに設定したイベントハンドラ実行時は、
eventPhase:1=キャプチャリングフェーズであることが分かります。

(2)内外共にuseCaptureにfalseを設定

この場合場合ターゲットノードの場合とバブリングフェーズにイベントハンドラが実行されます。
外側のdiv(id=outer2)をクリックすると、以下のように表示されます。

eventPhase:2,target:outer2,currentTarget:outer2

内側のdiv(id=inner2)をクリックした場合は、以下のように表示されます。

eventPhase:2,target:inner2,currentTarget:inner2
eventPhase:3,target:inner2,currentTarget:outer2

内側、外側という順にイベントハンドラが実行されていること、
外側のdivに設定したイベントハンドラ実行時は、
eventPhase:3=バブリングフェーズであることが分かります。

(3)内外共に、useCaptureにtrue・false両方でイベントハンドラを設定

外側のdiv(id=outer3)をクリックすると、以下のように表示されます。

eventPhase:2,target:outer3,currentTarget:outer3
eventPhase:2,target:outer3,currentTarget:outer3

内側のdiv(id=inner3)をクリックした場合は、以下のように表示されます。

eventPhase:1,target:inner3,currentTarget:outer3
eventPhase:2,target:inner3,currentTarget:inner3
eventPhase:2,target:inner3,currentTarget:inner3
eventPhase:3,target:inner3,currentTarget:outer3

前述の図のように外→内→外というようにイベントが伝播していることが分かります。

Event.stopPropagation()

イベントハンドラの引数であるEventオブジェクトのstopPropagationメソッドを実行すると、
それ以降のイベントの伝播を停止させられます。
どのフェーズであってもそのイベントハンドラ以降の伝播は停止します。

(4)内外共にuseCaptureにtrueを設定、外側でEvent.stopPropagation()を実行する

外側のdiv(id=outer4)をクリックすると、以下のように表示されます。

eventPhase:2,target:outer4,currentTarget:outer4

内側のdiv(id=inner4)をクリックした場合は、以下のように表示されます。

eventPhase:1,target:inner4,currentTarget:outer4

内側のdivをクリックした場合、外側のdivに設定されたイベントハンドラが
キャプチャリングフェーズで実行され、そこでイベントの伝播が止まっているのが分かります。
((1)と要比較)

(5)内外共にuseCaptureにfalseを設定、内側でEvent.stopPropagation()を実行する

外側のdiv(id=outer5)をクリックすると、以下のように表示されます。

eventPhase:2,target:outer5,currentTarget:outer5

内側のdiv(id=inner5)をクリックした場合は、以下のように表示されます。

eventPhase:2,target:inner5,currentTarget:inner5

内側のdivをクリックした場合、ターゲットノードでイベントハンドラが実行され、
イベント伝播が止まり、外側のdivのイベントハンドラが実行されません。
((2)と要比較)

(6)3つ入れ子のdivすべてでuseCaptureにfalseを設定、外側から2番目でEvent.stopPropagation()を実行する

一番外側のdiv(id=hoge)をクリックすると、以下のように表示されます。

eventPhase:2,target:hoge,currentTarget:hoge

間のdiv(id=outer6)、内側のdiv(id=inner6)をクリックした場合は、
それぞれ以下のように表示されます。

eventPhase:2,target:outer6,currentTarget:outer6
eventPhase:2,target:inner6,currentTarget:inner6
eventPhase:3,target:inner6,currentTarget:outer6

内側のdivから間のdivまではバブリングしてますが、
そこでイベント伝播は止まり、一番外側のイベントハンドラは実行されません。

とまあ

教科書どおりのことを一通り動かして検証してみました。
今後はAPIとしてはDOMレベル2イベントに準拠しているOperaやSafariで、
どのように動作が変わるかなどを検証したいと思います。