IEではRequireJS+jQueryで$(document).ready()で設定したcallbackが実行されないことがある&その対応

とりあえずRequireJS0.14.5+jQuery1.4.3,RequireJS0.15.0+jQuery1.4.4で発生。
IEは6~8っぽい。

なんで気づいたかというと

  1. IEでjQueryUI等の動作がおかしくなった
  2. CSS Box Modelがサポートされている状態なのに$.boxModel(=$.support.boxModel)がtrueでない
    • falseですらなくundefinedだった
  3. $.boxModelの検出はjQuery(function(){})内で行われていた。
    • $(document).ready()と同じ

検証

対象:RequireJS0.15.0+jQuery1.4.4
環境:IE7(+CompanionJS)
今、仕事で作ってるアプリで使っているallplugins-require.jsとjquery-1.4.4.jsに
以下のようにconsole.logを追加した。
後、仕事で作っているコードの中のrequire.ready callbackの頭に、
console.log('require.ready');というlog出力を追加した。


jQueryについてはjQuery.readyの中でlog出力し、
後はtimerやevent経由でjQuery.readyを呼んでいる箇所について、
どこで設定されたものが呼ばれたか分かるようにlog出力した。


$(document).ready()で設定されたcallbackは、
jQuery.readyによって実行されるが、準備が出来ていないと判断される場合は実行されない。
jQuery.readyWaitが0でjQuery.isReadyがtrueの場合が準備できている状態である。
そして、RequireJSは内部でjQuery.readyWaitを増減するので、
その箇所でlog出力をするようにした。

diff -r 4ba54b5cad32 static/js/jquery-1.4.4.js
--- a/static/js/jquery-1.4.4.js	Wed Nov 17 15:35:04 2010 +0900
+++ b/static/js/jquery-1.4.4.js	Wed Nov 17 20:29:59 2010 +0900
@@ -413,6 +413,7 @@
 	
 	// Handle when the DOM is ready
 	ready: function( wait ) {
+    console.log('jQuery.ready:'+jQuery.readyWait+','+jQuery.isReady);
 		// A third-party is pushing the ready event forwards
 		if ( wait === true ) {
 			jQuery.readyWait--;
@@ -454,7 +455,7 @@
 			}
 		}
 	},
-	
+
 	bindReady: function() {
 		if ( readyBound ) {
 			return;
@@ -466,7 +467,10 @@
 		// browser event has already occurred.
 		if ( document.readyState === "complete" ) {
 			// Handle it asynchronously to allow scripts the opportunity to delay ready
-			return setTimeout( jQuery.ready, 1 );
+			return setTimeout( function(){
+        console.log('jQuery.ready document.readyState === "complete"');
+        jQuery.ready();
+      }, 1 );
 		}
 
 		// Mozilla, Opera and webkit nightlies currently support this event
@@ -475,7 +479,10 @@
 			document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );
 			
 			// A fallback to window.onload, that will always work
-			window.addEventListener( "load", jQuery.ready, false );
+			window.addEventListener( "load", function(){
+        console.log('jQuery.ready window.onload');
+        jQuery.ready();
+      }, false );
 
 		// If IE event model is used
 		} else if ( document.attachEvent ) {
@@ -484,7 +491,10 @@
 			document.attachEvent("onreadystatechange", DOMContentLoaded);
 			
 			// A fallback to window.onload, that will always work
-			window.attachEvent( "onload", jQuery.ready );
+			window.attachEvent( "onload", function(){
+        console.log('jQuery.ready() window.onload');
+        jQuery.ready();
+      } );
 
 			// If IE and not a frame
 			// continually check to see if the document is ready
@@ -901,6 +911,7 @@
 	}
 
 	// and execute any waiting functions
+  console.log('jQuery.ready() doScrollCheck');
 	jQuery.ready();
 }
diff -r 4ba54b5cad32 static/js/allplugins-require.js
--- a/static/js/allplugins-require.js	Wed Nov 17 15:35:04 2010 +0900
+++ b/static/js/allplugins-require.js	Wed Nov 17 20:29:34 2010 +0900
@@ -704,6 +704,7 @@
                 if (context.scriptCount) {
                     $.readyWait += 1;
                     context.jQueryIncremented = true;
+                    console.log('jQueryCheck :' + $.readyWait + ',' + $.isReady); 
                 }
             }
         }
@@ -953,6 +954,7 @@
                 if (context.jQuery && !context.jQueryIncremented) {
                     context.jQuery.readyWait += 1;
                     context.jQueryIncremented = true;
+                    console.log('req.load :' + $.readyWait + ',' + $.isReady);
                 }
             }
         }
@@ -1679,6 +1681,7 @@
                     if (context.jQueryIncremented) {
                         context.jQuery.readyWait -= 1;
                         context.jQueryIncremented = false;
+                        console.log('req.callReady :' + $.readyWait + ',' + $.isReady);
                     }
                 }
             }

結果

Firefox3.6+Firebug1.5の場合
jQueryCheck :2,false
jQuery.ready:2,false
require.ready
req.callReady :0,true
jQuery.ready window.onload
jQuery.ready:0,true
  1. jQueryCheckが呼ばれてreadyStateがインクリメントされる
  2. (中略)
  3. require.ready callbackを呼び出し終えてreq.callReadyがデクリメントされ0になる
  4. window.onloadのタイミングでjQuery.readyが呼ばれる
    • $(document).ready()で設定したcallbackが実行される
IE7+CompanionJSの場合
Console [332]=     
 jQueryCheck :2,false
 
Console [333]=     
 jQuery.ready:2,false
 
Console [334]=     
 jQuery.ready() window.onload
 
Console [335]=     
 jQuery.ready:1,true
 
Console [336]=     
 require.ready
 
Console [337]=     
 req.callReady :0,true
 
  1. jQueryCheckが呼ばれてreadyStateがインクリメントされる
  2. (中略)
  3. window.onloadのタイミングでjQuery.readyが呼ばれる
    • readyState≠0なので、$(document).ready()で設定したcallbackが実行されない
  4. require.ready callbackを呼び出し終えてreq.callReadyがデクリメントされ0になる

jQueryによってjQuery.readyが呼ばれる最後のタイミングはwindow.onloadなので、
$(document).ready()で設定したcallbackは何時まで経っても実行されない。

フィードバックとか

仕事のコード抜きで再現させられたらしようかと。

とりあえずの対策

require.ready callbackとreq.callReadyの実行は同一のrun loopなので、
以下のような内容のJavaScriptをrequireしてやれば良い。

(function () {
  var windowLoaded = false;
  $(window).bind('load',function(){
    windowLoaded = true;
  });
  require.ready(function() {
    if ($.readyWait !== 0 && windowLoaded) {
      setTimeout(function() {
        $.ready();
      });
    }
  });
})();
Console [360]=     
 jQueryCheck :2,false
 
Console [361]=     
 jQuery.ready:2,false
 
Console [362]=     
 jQuery.ready() window.onload
 
Console [363]=     
 jQuery.ready:1,true
 
Console [364]=     
 require.ready
 
Console [365]=     
 req.callReady :0,true
 
Console [366]=     
 jQuery.ready:0,true