jquery.jsを読み解く

第10回jQueryライブラリ(2183行目~2364行目)

今回もイベントの処理に関する部分の解説になります。$(document).ready()を実装している部分が出てきますが、ここのソースコードは非常に興味深いです。よくある実装方法としては、動的にscriptタグを挿入してJavaScriptコードを実行する方法がありますが、jQueryの実装はもっと複雑なものです。また、DOMContentLoadedを利用して、処理の開始をできるだけ早くして、ユーザの体感速度を向上するための工夫も大変参考になります。

それでは、順に見ていきましょう。

jQuery.fn.bind()

2183: jQuery.fn.extend({
2184:   bind: function( type, data, fn ) {
2185:     return type == "unload" ? this.one(type, data, fn) : this.each(function(){
2186:       jQuery.event.add( this, type, fn || data, fn && data );
2187:     });
2188:   },
2189:   

jQuery.fn.bind()メソッドは、現在選択されている要素にイベントを割り当てます。ここで、第一引数のtypeには、"click"、"mouseover"などのイベント種別が渡されてきます。また、第二引数のdataには、イベントハンドラに引き継ぐデータが渡されてきます。最後に、第3引数のfnがイベントハンドラ関数になります。

2185行目で、unloadイベントの場合のみone()メソッドを呼び出し、それ以外の場合は1804行目第8回参照のjQuery.evnet.add()メソッドに処理を引き継ぎます。

jQuery.fn.one()

2190:   one: function( type, data, fn ) {
2191:     return this.each(function(){
2192:       jQuery.event.add( this, type, function(event) {
2193:         jQuery(this).unbind(event);
2194:         return (fn || data).apply( this, arguments);
2195:       }, fn && data);
2196:     });
2197:   },
2198: 

jQuery.fn.one()メソッドは、引数のイベントハンドラ関数fnを1回だけ実行するように割り当てます。

2192行目でjQuery.event.add()メソッドを呼び出すのは先ほどのbind()メソッドと同様ですが、2193行目でハンドラ関数の前にunbind()によってイベントを解除するように設定しているところが違います。

jQuery.fn.unbind()

2199:   unbind: function( type, fn ) {
2200:     return this.each(function(){
2201:       jQuery.event.remove( this, type, fn );
2202:     });
2203:   },
2204: 

jQuery.fn.unbind()メソッドは、登録されているイベントを解除します。実際の処理は、2201行目で呼び出しているjQuery.event.remove()メソッドによって実行されます。

jQuery.fn.trigger()

2205:   trigger: function( type, data, fn ) {
2206:     return this.each(function(){
2207:       jQuery.event.trigger( type, data, this, true, fn );
2208:     });
2209:   },
2210: 

jQuery.fn.trigger()メソッドは、jQuery.event.trigger()メソッドのラッパーです。2207行目で、各要素に指定されたイベントを実行します。

jQuery.fn.triggerHandler()

2211:   triggerHandler: function( type, data, fn ) {
2212:     if ( this[0] )
2213:       return jQuery.event.trigger( type, data, this[0], false, fn );
2214:     return undefined;
2215:   },
2216: 

jQuery.fn.triggerHandler()メソッドも、指定されたイベントを実行するのですが、ブラウザ標準のアクションを実行しないところが違います。このため、2213行目でjQuery.event.trigger()メソッドを呼び出す際に第4引数としてfalseを指定します。

jQuery.fn.toggle()

2217:   toggle: function() {
2218:     // Save reference to arguments for access in closure
2219:     var args = arguments;
2220: 
2221:     return this.click(function(event) {
2222:       // Figure out which function to execute
2223:       this.lastToggle = 0 == this.lastToggle ? 1 : 0;
2224:       
2225:       // Make sure that clicks stop
2226:       event.preventDefault();
2227:       
2228:       // and execute the function
2229:       return args[this.lastToggle].apply( this, arguments ) || false;
2230:     });
2231:   },
2232: 

jQuery.fn.toggle()メソッドは、引数に指定されたイベントハンドラを交互に実行します。2219行目は、クロージャ内から引数にアクセスするために値を保存しています。

2223行目は、this.lastToggleに0と1を交互に設定するための処理で、現在の値が0ならば1を、1ならば0が割り当てられます。これにより2229行目で引数に指定されたどちらの関数を実行されるかが決まります。2226行目は、ブラウザ標準のclickイベントを実行させないための予防処理になります。

2229行目は、実行時にならないとthisが分からないため、apply関数を利用して呼び出します。

jQuery.fn.hover()

2233:   hover: function(fnOver, fnOut) {
2234:     return this.bind('mouseenter', fnOver).bind('mouseleave', fnOut);
2235:   },
2236:   

jQuery.fn.hover()メソッドは、選択された要素に自身の'mouseenter'および'mouseleave'イベントを割り当てます。引数に指定されたfnOverおよびfnOutがそれぞれのイベントハンドラになります。

jQuery.fn.ready()

2237:   ready: function(fn) {
2238:     // Attach the listeners
2239:     bindReady();
2240: 
2241:     // If the DOM is already ready
2242:     if ( jQuery.isReady )
2243:       // Execute the function immediately
2244:       fn.call( document, jQuery );
2245:       
2246:     // Otherwise, remember the function for later
2247:     else
2248:       // Add the function to the wait list
2249:       jQuery.readyList.push( function() { return fn.call(this, jQuery); } );
2250:   
2251:     return this;
2252:   }
2253: });
2254: 

jQuery.fn.ready()メソッドは、jQueryライブラリの中でも重要な位置を占める$(document).ready()を定義している部分になります。Webアプリケーションの実行速度が決まる重要な部分であり、とても凝った作りになっています。

まず、2239行目にて、2284行目で定義されているbindReady()メソッドを実行します。bindReady()メソッドは、ブラウザごとの違いを吸収しつつDOMの準備ができたことを検知するためのめそっどになります。

2242行目は、DOMの準備ができているかどうかを確認し、準備できていれば即座に関数を実行します。もし、できていなければ、jQuery.readyListのキューに入れておいて、準備ができた時点で実行されるようにします。

jQuery.ready()

2255: jQuery.extend({
2256:   isReady: false,
2257:   readyList: [],
2258:   // Handle when the DOM is ready
2259:   ready: function() {
2260:     // Make sure that the DOM is not already loaded
2261:     if ( !jQuery.isReady ) {
2262:       // Remember that the DOM is ready
2263:       jQuery.isReady = true;
2264:       
2265:       // If there are functions bound, to execute
2266:       if ( jQuery.readyList ) {
2267:         // Execute all of them
2268:         jQuery.each( jQuery.readyList, function(){
2269:           this.apply( document );
2270:         });
2271:         
2272:         // Reset the list of functions
2273:         jQuery.readyList = null;
2274:       }
2275:     
2276:       // Trigger any bound ready events
2277:       jQuery(document).triggerHandler("ready");
2278:     }
2279:   }
2280: });
2281: 

2256行目のisReadyは、DOMの準備ができているかどうかを表す変数で初期値はfalseです。

2257行目のreadyListには、準備ができた際に実行される関数が格納されます。

2259行目のjQuery.ready()メソッドは、2284行目で定義されているbindReady()メソッドから、DOMの準備ができた際に呼び出されます。2261行目のif文により、既にjQuery.isReadyがtrueであれば、何もしません。そうでなければ、jQuery.isReadyをtrueに設定し、jQuery.readyListに設定された関数を登録された順番に実行していきます。実行後、2273行目にてjQuery.readyListの中身を空にします。

最後に2277行目で、その他に設定されたreadyイベント実行するために、triggerHandler()メソッドを呼び出します。

bindReady()

2282: var readyBound = false;
2283: 
2284: function bindReady(){
2285:   if ( readyBound ) return;
2286:   readyBound = true;
2287: 
2288:   // Mozilla, Opera (see further below for it) and webkit nightlies currently support this event
2289:   if ( document.addEventListener && !jQuery.browser.opera)
2290:     // Use the handy event callback
2291:     document.addEventListener( "DOMContentLoaded", jQuery.ready, false );
2292:   
2293:   // If IE is used and is not in a frame
2294:   // Continually check to see if the document is ready
2295:   if ( jQuery.browser.msie && window == top ) (function(){
2296:     if (jQuery.isReady) return;
2297:     try {
2298:       // If IE is used, use the trick by Diego Perini
2299:       // http://javascript.nwbox.com/IEContentLoaded/
2300:       document.documentElement.doScroll("left");
2301:     } catch( error ) {
2302:       setTimeout( arguments.callee, 0 );
2303:       return;
2304:     }
2305:     // and execute any waiting functions
2306:     jQuery.ready();
2307:   })();
2308: 
2309:   if ( jQuery.browser.opera )
2310:     document.addEventListener( "DOMContentLoaded", function () {
2311:       if (jQuery.isReady) return;
2312:       for (var i = 0; i 2313:         if (document.styleSheets[i].disabled) {
2314:           setTimeout( arguments.callee, 0 );
2315:           return;
2316:         }
2317:       // and execute any waiting functions
2318:       jQuery.ready();
2319:     }, false);
2320: 
2321:   if ( jQuery.browser.safari ) {
2322:     var numStyles;
2323:     (function(){
2324:       if (jQuery.isReady) return;
2325:       if ( document.readyState != "loaded" && document.readyState != "complete" ) {
2326:         setTimeout( arguments.callee, 0 );
2327:         return;
2328:       }
2329:       if ( numStyles === undefined )
2330:         numStyles = jQuery("style, link[rel=stylesheet]").length;
2331:       if ( document.styleSheets.length != numStyles ) {
2332:         setTimeout( arguments.callee, 0 );
2333:         return;
2334:       }
2335:       // and execute any waiting functions
2336:       jQuery.ready();
2337:     })();
2338:   }
2339: 
2340:   // A fallback to window.onload, that will always work
2341:   jQuery.event.add( window, "load", jQuery.ready );
2342: }
2343: 

bindReady()メソッドは、DOMの準備ができたかどうかを判定するための関数です。ブラウザごとに判定方法が異なるため、処理が分かれています。まず、2285行目ですが、readyBound == trueすなわち、一度でもこのbindReady()関数が実行されていたら何もせずに処理を戻します。そうでなければ、readyBoundにtrueを設定します。そして、次の行からがブラウザごとの判定処理になります。

2289~2291行目は、MozillaなどDOMContentLoadedイベントが利用可能な場合にDOMContentLoadedイベント発生時に先ほどのjQuery.ready()メソッドを実行するように設定します。

2295~2307行目は、Internet Explorerでフレーム内からの呼び出しでない場合です。IEの場合はDOMContentLoadedイベントがないので、document.documentElement.doScroll("left")が使えるようになるかどうかを繰り返し監視して、doScrollメソッドが使えるようになったら、jQuery.ready()メソッドを実行します。2302行目は、arguments.calleeつまりこの無名関数自身を繰り返し実行するための処理です。

2309~2319行目は、Operaの場合の処理です。addEventListenerによってDOMContentLoadedイベントを監視するのはMozillaの時と同様なのですが、2312行目のforループによってdocument.styleSheetsが利用可能になるまでウェイトする部分が異なります。これは、Operaの場合は、DOMContentLoadedイベントが発生した時点ではスタイルシートに関する処理が完了していない問題を回避するための処理です。

2321~2338行目は、Safariの場合の処理です。SafariもDOMContentLoadedをサポートしていないので、こちらは少々複雑です。まず、2325行目でdocument.readyStateがloadedまたはcompleteになるのをひたすら待ちます。次に2330行目で、jQuery("style, link[rel=stylesheet]").lengthを実行して、styleタグと読み込まれているCSSファイルの数をチェックします。そして、この数がdocument.styleSheets.lengthと一致するまで待ちます。つまり、Operaの時と同様にスタイルの読み込みが完了するのを判定しているわけです。そして、この2つが完了してようやく、2336行目でjQuery.ready()メソッドを実行します。

これまでのどのブラウザにも当てはまらなかった場合は、最後の手段として、2341行目にてwindow.onloadイベントを設定します。ここで疑問になるのは、どうして最初からwindow.onloadイベントを利用しないのかということですが、JavaScriptを使ったアプリケーションにとって、$(document).ready()の実行開始はアプリケーションの動作開始を意味します。この実行開始のタイミングをできるだけ早くすることで、ユーザの体感速度の向上が図れます。window.onloadだと、画像のロードも含めてページの読み込みが完全に完了したタイミングになるのですが、jQueryの動作に必要のはDOMおよびstyleの読み込みが完了したタイミングです。これを知るために、ブラウザごとに複雑な判別処理をしているわけです。

各種イベントメソッドの登録

2344: jQuery.each( ("blur,focus,load,resize,scroll,unload,click,dblclick," +
2345:   "mousedown,mouseup,mousemove,mouseover,mouseout,change,select," + 
2346:   "submit,keydown,keypress,keyup,error").split(","), function(i, name){
2347:   
2348:   // Handle event binding
2349:   jQuery.fn[name] = function(fn){
2350:     return fn ? this.bind(name, fn) : this.trigger(name);
2351:   };
2352: });
2353: 

2344行目からは、各種イベントメソッドをjQuery.fnオブジェクトに登録する部分になります。2344行目から列挙されているカンマで区切られたそれぞれのメソッドを登録します。2349~2350行目が登録するメソッドの実体で、引数が渡されてきたら引数に指定された関数をイベントハンドラとして割り当てます。引数が渡されてこなければ、trigger()を使ってそのメソッドを実行します。どちらが実行されるかは、実際にこれらのメソッドが呼び出された時点で決まります。

withinElement()

2354: // Checks if an event happened on an element within another element
2355: // Used in jQuery.event.special.mouseenter and mouseleave handlers
2356: var withinElement = function(event, elem) {
2357:   // Check if mouse(over|out) are still within the same parent element
2358:   var parent = event.relatedTarget;
2359:   // Traverse up the tree
2360:   while ( parent && parent != elem ) try { parent = parent.parentNode; } catch(error) { parent = elem; }
2361:   // Return true if we actually just moused on to a sub-element
2362:   return parent == elem;
2363: };
2364: 

2356行目からは、イベントが他の要素内で起こっているかどうかをチェックするための関数です。2358行目のevent.relatedTargetは、mouseover/mouseoutのイベント発生時にマウスカーソルがどこから来たのか、もしくはどこへ行ったのかという値を取得します。そして、2360行目で親要素を再帰的に探して、2362行目でその親要素が第2引数と等しいかどうかを判定して返します。

おすすめ記事

記事・ニュース一覧