jQueryの$(document).readyをjQuery非依存にする。

2008/09/09追記

GitHubに置いてみた。これくらいなら、Gistで良かったかも。
bindready/bindReady.js at master · monjudoh/bindready · GitHub


jQueryを使っていないWebサイトで必要最小限のJavaScriptを使って、
DOM構築後のタイミングで実行したい処理を実行したい。

コードリーディング(概略)

とりあえずjQuery1.2.6の該当箇所のコードを読む…のは、
$(document).ready();について - 文殊堂でやった。

省く機能

DOM構築後に呼び出された時即実行→何もしない
jQuery.readyListにpushしてDOM構築後に複数の関数を順繰り実行→一つだけ実行
いろいろくるんでthisとか第一引数とかにいろいろ指定→何もせずに関数をそのまま実行

要はbindReady(someFunction);とやると、
DOM構築後のタイミングでsomeFunction();となるだけの関数を作るとする。

コードリーディング(依存列挙)

2329-2387行目

function bindReady(){
	if ( readyBound ) return;
	readyBound = true;

	// Mozilla, Opera (see further below for it) and webkit nightlies currently support this event
	if ( document.addEventListener && !jQuery.browser.opera)
		// Use the handy event callback
		document.addEventListener( "DOMContentLoaded", jQuery.ready, false );

	// If IE is used and is not in a frame
	// Continually check to see if the document is ready
	if ( jQuery.browser.msie && window == top ) (function(){
		if (jQuery.isReady) return;
		try {
			// If IE is used, use the trick by Diego Perini
			// http://javascript.nwbox.com/IEContentLoaded/
			document.documentElement.doScroll("left");
		} catch( error ) {
			setTimeout( arguments.callee, 0 );
			return;
		}
		// and execute any waiting functions
		jQuery.ready();
	})();

	if ( jQuery.browser.opera )
		document.addEventListener( "DOMContentLoaded", function () {
			if (jQuery.isReady) return;
			for (var i = 0; i < document.styleSheets.length; i++)
				if (document.styleSheets[i].disabled) {
					setTimeout( arguments.callee, 0 );
					return;
				}
			// and execute any waiting functions
			jQuery.ready();
		}, false);

	if ( jQuery.browser.safari ) {
		var numStyles;
		(function(){
			if (jQuery.isReady) return;
			if ( document.readyState != "loaded" && document.readyState != "complete" ) {
				setTimeout( arguments.callee, 0 );
				return;
			}
			if ( numStyles === undefined )
				numStyles = jQuery("style, link[rel=stylesheet]").length;
			if ( document.styleSheets.length != numStyles ) {
				setTimeout( arguments.callee, 0 );
				return;
			}
			// and execute any waiting functions
			jQuery.ready();
		})();
	}

	// A fallback to window.onload, that will always work
	jQuery.event.add( window, "load", jQuery.ready );
}

この中からjQueryのほかの箇所に依存している箇所を列挙

依存除去

readyBound
function bindReady(){
	if ( readyBound ) return;
	readyBound = true;
	/* ブラウザ別イベント登録処理 */
}

これは初回呼び出し時のみ実処理を実行するということなので、
以下のコードで代替させる。

function bindReady(){
	bindReady = function(){};
	/* ブラウザ別イベント登録処理 */
}
jQuery.browser.operajQuery.browser.msie

これらのプロパティの元々の定義箇所は1221-1230行目

var userAgent = navigator.userAgent.toLowerCase();

// Figure out what browser is being used
jQuery.browser = {
	version: (userAgent.match( /.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/ ) || [])[1],
	safari: /webkit/.test( userAgent ),
	opera: /opera/.test( userAgent ),
	msie: /msie/.test( userAgent ) && !/opera/.test( userAgent ),
	mozilla: /mozilla/.test( userAgent ) && !/(compatible|webkit)/.test( userAgent )
};

bindReady関数は一度しか実行されないのでこれらも関数内で定義してしまって問題ない。
jQuery.browser→browserとして、使用していないプロパティの定義をやめる。

function bindReady(){
	bindReady = function(){};
	var userAgent = navigator.userAgent.toLowerCase();
	var browser = {
		safari: /webkit/.test( userAgent ),
		opera: /opera/.test( userAgent ),
		msie: /msie/.test( userAgent ) && !/opera/.test( userAgent )
	};
	/* ブラウザ別イベント登録処理 */
}
jQuery.isReady・jQuery.ready

だいぶ機能を削ったので全部再現する必要はない
それぞれjQueryのプロパティからローカル変数に変える

function bindReady(callback){
	var isReady = false;
	function ready(){
		if(isReady)return;
		isReady = true;
		callback();
	}
	bindReady = function(){};
	var userAgent = navigator.userAgent.toLowerCase();
	var browser = {
		safari: /webkit/.test( userAgent ),
		opera: /opera/.test( userAgent ),
		msie: /msie/.test( userAgent ) && !/opera/.test( userAgent )
	};
	/* ブラウザ別イベント登録処理 */
}
jQuery.event.add
jQuery.event.add( window, "load", jQuery.ready );

使用箇所これだけ実質window.onloadに設定できればOKなはず。

function bindReady(callback){
	var isReady = false;
	function ready(){
		if(isReady)return;
		isReady = true;
		callback();
	}
	bindReady = function(){};
	var userAgent = navigator.userAgent.toLowerCase();
	var browser = {
		safari: /webkit/.test( userAgent ),
		opera: /opera/.test( userAgent ),
		msie: /msie/.test( userAgent ) && !/opera/.test( userAgent )
	};
	/* ブラウザ別イベント登録処理 */
	var oldOnload = window.onload;
	window.onload=function(){
		if(oldOnload)oldOnload();
		ready();
	};
}
jQuery("style, link[rel=stylesheet]")

必要なのは↓

jQuery("style, link[rel=stylesheet]").length

なので代替コードを関数にしてやればよい

function countNumStyles(){
	var d = document;
	var stylesLength = d.getElementsByTagName('style').length;
	var links = d.getElementsByTagName('link');
	for(var i = 0; i++; i < links.length){
		if(links[i].rel == 'stylesheet'){
			stylesLength++;
		}
	}
	return stylesLength;
}

コード

こんな感じになった。

function bindReady(callback){
	var isReady = false;
	function ready(){
		if(isReady)return;
		isReady = true;
		callback();
	}
	bindReady = function(){};
	var userAgent = navigator.userAgent.toLowerCase();
	var browser = {
		safari: /webkit/.test( userAgent ),
		opera: /opera/.test( userAgent ),
		msie: /msie/.test( userAgent ) && !/opera/.test( userAgent )
	};
	// Mozilla, Opera (see further below for it) and webkit nightlies currently support this event
	if ( document.addEventListener && !browser.opera)
		// Use the handy event callback
		document.addEventListener( "DOMContentLoaded", ready, false );

	// If IE is used and is not in a frame
	// Continually check to see if the document is ready
	if ( browser.msie && window == top ) (function(){
		if (isReady) return;
		try {
			// If IE is used, use the trick by Diego Perini
			// http://javascript.nwbox.com/IEContentLoaded/
			document.documentElement.doScroll("left");
		} catch( error ) {
			setTimeout( arguments.callee, 0 );
			return;
		}
		// and execute a waiting function
		ready();
	})();

	if ( browser.opera )
		document.addEventListener( "DOMContentLoaded", function () {
			if (isReady) return;
			for (var i = 0; i < document.styleSheets.length; i++)
				if (document.styleSheets[i].disabled) {
					setTimeout( arguments.callee, 0 );
					return;
				}
			// and execute a waiting function
			ready();
		}, false);

	if ( browser.safari ) {
		var numStyles;
		function countNumStyles(){
			var d = document;
			var stylesLength = d.getElementsByTagName('style').length;
			var links = d.getElementsByTagName('link');
			for(var i = 0; i++; i < links.length){
				if(links[i].rel == 'stylesheet'){
					stylesLength++;
				}
			}
			return stylesLength;
		}
		(function(){
			if (isReady) return;
			if ( document.readyState != "loaded" && document.readyState != "complete" ) {
				setTimeout( arguments.callee, 0 );
				return;
			}
			if ( numStyles === undefined )
				numStyles = countNumStyles();
			if ( document.styleSheets.length != numStyles ) {
				setTimeout( arguments.callee, 0 );
				return;
			}
			// and execute a waiting function
			ready();
		})();
	}
	var oldOnload = window.onload;
	window.onload=function(){
		if(oldOnload)oldOnload();
		ready();
	};
}

とりあえずIE6.0、Firefox3.0.1、Opera9.52、Safari3.1、Google Chrome 0.2.149.27 Beta(すべてWindows版)で確認

てきとーにファイルサイズのでかいHTMLをダウンしてきて、
HEAD内に

<script type="text/javascript">
(function(){
function bindReady(callback){
/*中略*/
bindReady(function(){
alert(document.getElementById('hogehoge').innerHTML);
});
})();
</script>

BODYの最後に

<span id="hogehoge">fugafuga</span>

とかやってみたら上手くいった。
ここのところのjQueryの元のコードだとテストとかどうしてるんだろ?