読者です 読者をやめる 読者になる 読者になる
スポンサーリンク

JavaScriptの動かないコード (中級編) 動的追加したイベントの実行順序 ( addEventListener vs attachEvent )

javascript 動かないコード


以下のJavaScriptコードが意図した動作をしないのは,なぜですか。(制限時間1分)


やりたい事:

  • ボタンへ動的にイベントリスナを追加する。
  • ボタン押下時に,0, 1, 2 とアラート表示する。
<input type="button" value="「0, 1, 2」と表示" id="my_button">

<script language="JavaScript">

function f0(){ alert( 0 ); }
function f1(){ alert( 1 ); }
function f2(){ alert( 2 ); }

// イベントハンドラ設定
function addEvent( element, eventName, func ){  
	if ( element.addEventListener )
	{
		// Firefox用
		element.addEventListener( eventName, func, false );
	}
	else if ( element.attachEvent )
	{
		// IE用
		element.attachEvent( "on" + eventName, func );  
	}
}

// リスナーを追加
addEvent( my_button, "click", f0 );
addEvent( my_button, "click", f1 );
addEvent( my_button, "click", f2 );

</script>




答え



FFでは「0, 1, 2」と表示されるが,
IEでは「1, 2, 0」と表示される。


IEのイベント実行順序がおかしい。


イベントリスナの追加については,次のような説明がなされる事がある。


Internet Explorer では,後から追加したイベントリスナーが先に実行されます。


一方,その他のWebブラウザでは,先に追加したイベントリスナーから順に実行され,Internet Explorer とは順序が逆になります。


…(「JavaScript 中級講座 Ajaxを学ぶ前の基礎知識」,技術評論社 より抜粋)


この1文目は誤りである。(本は良書なのだけど)

確かにFirefoxでは,リスナーは追加順に実行される。


でもIEでは,冒頭のコードで見たように,必ずしも逆順に実行されるとは限らない。
ただし追加した関数が2つの場合は,最後に追加したものが最初に実行されるので,逆順に実行されたように見える。


追加順でもないし,追加の逆順でもないという事は,では一体どういう順序で実行されるのだろうか?

IEでのイベントリスナー実行順序を検証する

リスナが1個〜10個の場合のそれぞれについて,実行順序を確かめてみよう。

そのテストを実行するコードを下記に掲載した。

<input type="button" value="テストを実行" onClick="exec()">
<input type="button" value="イベントリスナ追加用のボタン" id="my_button">

<br>

<div id="log"></div>


<script language="JavaScript">

// リスナ追加
function addEvent( element, eventName, func )
{
	if ( element.addEventListener ){ element.addEventListener( eventName, func, false ); }
	else if ( element.attachEvent ){ element.attachEvent( "on" + eventName, func );      }
}

// リスナ削除
function removeEvent( element, eventName, func )
{
	if ( element.removeEventListener ){ element.removeEventListener( eventName, func, false ); }
	else if ( element.detachEvent )   { element.detachEvent( "on" + eventName, func );         }
}


// テストを実行
function exec()
{
	// 追加する最大数(ここでは10個のリスナ)
	var max = 10;

	// 関数を max 個作成
	for( var i = 1; i <= max; i ++ )
	{
		// 関数をグローバル変数で(var無しで)宣言する
		var str = "f"
			+ i 
			+ " = function (){ log.innerHTML += '" 
			+ i 
			+ " ';}"
			;
		eval( str );
	}
	
	// テスト実行
	for( var i = 1; i <= max; i ++ )
	{
		execCase( i );
	}
}

// リスナを num 個追加した場合の実行順序を調べる
function execCase( num )
{
	// リスナを num 個追加
	for( var j = 1; j <= num; j ++ )
	{
		addEvent( my_button, "click", eval( "f" + j ) );
	}

	// イベントを発生させる
	my_button.click();

	// リスナを num 個削除
	for( var j = 1; j <= num; j ++ )
	{
		removeEvent( my_button, "click", eval( "f" + j ) );
	}
	log.innerHTML += "<br>";
}


</script>


「テストを実行」というボタンを押すと,テストが始まる。

実行結果は,手元の環境では下記のようになった。


Firefox 3.0.3:

1
1 2
1 2 3
1 2 3 4
1 2 3 4 5
1 2 3 4 5 6
1 2 3 4 5 6 7
1 2 3 4 5 6 7 8
1 2 3 4 5 6 7 8 9
1 2 3 4 5 6 7 8 9 10

これは予想通りの実行順序で,理想の挙動。


それに対し,IE 7:

1
2 1
2 3 1
2 3 4 1
5 2 3 4 1
6 5 2 3 4 1
6 7 5 2 3 4 1
6 8 7 5 2 3 4 1
6 8 9 7 5 2 3 4 1
6 8 10 9 7 5 2 3 4 1

これはひどい・・・全く傾向がつかめない。


しかも,他のお方が試した場合には,さらに実行順序が変わる。

  • IEのイベント実行順序は不定?

http://d.hatena.ne.jp/inamenai/200809...


イベントが3件以上登録されていると、逆順でもないようです(10件登録して実行すると、3→2→4→6→8→10→9→7→5→1 になる)。

何度リロードしても同じ順番なので何らかの法則性はあるみたいですが、どういう規則なのかよくわかりません…。

ちなみに、 prototype.jsを使わずに直接attachEvent()を書いた場合は、2→3→5→7→9→10→8→6→4→1 とまた違う順番になってますます意味不明。

対策

「イベントリスナの定義が一つの関数内に収まる」のならば,追加は1つで済むので,面倒な事が起こらず一番よいだろう。


また複数の定義が必要な場合であっても,「イベントリスナ間で実行順序に依存性を持たせない」という方針が可能かもしれない。

あるリスナ関数は画面上のある部分を操作し,別のリスナ関数は画面上の他の部分を操作し,・・・といった具合に,処理を分散させるのだ。



では,冒頭のコードのように,複数のイベントリスナーの実行順序が重要になる場合はどうか?


もし,ある関数の実行前に,外部ライブラリの読み込み済み判定が必要なのであれば,amachangによる下記のエントリが参考になるかもしれない。

動的ローディング雑感
http://d.hatena.ne.jp/amachang/200808...

これは個別のscriptタグにコールバック関数を設ける方法。



あるいは,IEでどうしてもイベントリスナの実行順序を保証したい,という場合は,スレッドみたいな事をするという手がある。

この具体的なコード例は,連載外で別途,次記事に書く事にする。