JavaScriptで,動的に追加されたイベントリスナの実行順序を保証する方法
前記事では下記のような事を述べた。
- Firefoxのイベントリスナーが複数ある場合,追加された順に実行される。
- IEのイベントリスナーが複数ある場合,追加順には実行されないし,追加順の逆順でもない。実行順序を予測する事はたいへん難しい。
- IE対策として,色々な方法があるが,スレッドみたいなことをすればよいのではないか。
たいした事をするわけでは無いのだけども,簡単に言うと「各イベントリスナーが自分の実行順を知っている」という状況を作ればよいのだ。
これは,「自分より前のプロセスが終わるまで眠る(sleep)」と言いかえられる。
ちょうどミューテックスのようなものになる。(下記URL参照)
スレッド間の同期を取るための同期オブジェクト
http://www.ne.jp/asahi/yamashita/prog...
この方法でいけば,イベント発生時に,イベントハンドラ内で,イベントリスナの「鎖」を作ることができる。
簡単な例を下記に作ってみた。
下記では,f0 というリスナを直接追加するのではなく,tryEventという関数を間接的に挟んでいる。
tryEvent() は呼び出し時にリスナと実行順を指定することができて,内部では,ひとつ前までのリスナが実行されたかどうかのチェックを行なっている。
まだ自分の出番が来ていない場合,他のリスナ関数に出番を譲り,自分は200ミリ秒眠る。
<input type="button" value="追加されたリスナを実行" id="my_button"> <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 f0() { alert(0); } function f1() { alert(1); } function f2() { alert(2); } // リスナ追加 addEvent( my_button, "click", function(){ tryEvent( f0, 0 ); } ); addEvent( my_button, "click", function(){ tryEvent( f1, 1 ); } ); addEvent( my_button, "click", function(){ tryEvent( f2, 2 ); } ); // 終了フラグ // true : 実行済み // false : まだ実行されていない var complete_flag = [ false, false, false ]; // 各リスナのクッションとなる関数 // func : 実行する関数 // num : 何番目に実行するのか function tryEvent( func, num ) { // num番目を実行するタイミングが来たか? var exec_flag = true; for( var i = 0; ( i < num )&&( exec_flag ); i ++ ) { // 自分より前に未実行のものがあったらfalse exec_flag = exec_flag && complete_flag[ i ]; } if( exec_flag ) { // 実行 func(); // 実行済み記録 complete_flag[ num ] = true; } else { // 保留 with( this ){ // 200ミリ秒後に再度確認 setTimeout( function(){ tryEvent( func, num ) }, 200 ); } } } </script>
この場合,FFでは「0, 1, 2」のアラートが出るし,IEでも「0, 1, 2」になる。
前記事のIEでの実行結果が「1, 2, 0」だった事を考えると,これは改善されたと言えるだろう。
動作確認の結果,追加したいイベントリスナーが10個に増えても大丈夫だった。
上のコードでは「終了フラグ」を手作業で作っている。
もし addEvent関数を工夫して,イベントを追加したいエレメントごとに自動で終了フラグを生成+初期化するようにすれば,特別な注意を払うことなく実行順序を保証できる。( イベント追加時の tryEvent という表記も不要になる。)
(※なお,thisに強制的に定義時スコープを割り当てるために,setTimeoutでwith文を使っているのがミソ。)
注意点
注意点としてこの場合,「眠っている時間」だけ,ブランクがどうしても生じる事になる。
だから,onloadやonclickでイベントのトリガが与えられたとしても,そのトリガとぴったり合ったタイミングで各リスナが実行されているわけではない。
アイデアとしてはまあいいかと思う。
上記は応急処置的なコードなので,機会があれば,IEでのリスナ実行順序がどうなっているのかツリーで可視化したい。
補足
addEvent関数だけで済むようにした。
<input type="button" value="追加されたリスナを実行(1)" id="my_button1"> <input type="button" value="追加されたリスナを実行(2)" id="my_button2"> <script language="JavaScript"> // リスナ追加 function addEvent( element, eventName, func ) { // 実行フラグを設定 if( ! ( "event_complete_flags" in element ) ) { element.event_complete_flags = new Array(); } element.event_complete_flags.push( false ); var exec_order = element.event_complete_flags.length - 1; // 各リスナのクッションとなる関数 // func : 実行する関数 // num : 何番目に実行するのか function tryEvent( element, func, num ) { // num番目を実行するタイミングが来たか? var exec_flag = true; for( var i = 0; ( i < num )&&( exec_flag ); i ++ ) { // 自分より前に未実行のものがあったらfalse exec_flag = exec_flag && element.event_complete_flags[ i ]; } if( exec_flag ) { // 実行 func(); // 実行済み記録 element.event_complete_flags[ num ] = true; } else { // 保留 with( this ){ // 200ミリ秒後に再度確認 setTimeout( function(){ tryEvent( element, func, num ) }, 200 ); } } } // イベントをセット with({ element : element, func : func, exec_order : exec_order, tryEvent : tryEvent }) { // DOM準拠ブラウザ用(Firefox等) if ( element.addEventListener ) { element.addEventListener( eventName, function(){ tryEvent( element, func, exec_order ); }, false ); } // DOM非準拠ブラウザ用(IE等) else if ( element.attachEvent ) { element.attachEvent( "on" + eventName, function(){ tryEvent( element, func, exec_order ); } ); } } } // 実行したい関数 function f0() { alert(0); } function f1() { alert(1); } function f2() { alert(2); } function f3() { alert(3); } // リスナ追加 addEvent( my_button1, "click", f0 ); addEvent( my_button1, "click", f1 ); addEvent( my_button1, "click", f2 ); addEvent( my_button1, "click", f3 ); addEvent( my_button2, "click", f3 ); addEvent( my_button2, "click", f2 ); addEvent( my_button2, "click", f1 ); addEvent( my_button2, "click", f0 ); </script>