JavaScriptの動かないコード (中級編) 動的追加したイベントの実行順序 ( addEventListener vs attachEvent )
以下の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でどうしてもイベントリスナの実行順序を保証したい,という場合は,スレッドみたいな事をするという手がある。
この具体的なコード例は,連載外で別途,次記事に書く事にする。