search phpbb-phpbb-FC2BLOG-Info-Edit Template-Post-Edit-Upload-LogOut
1.2.2 ではまたしても jQuery オブジェクトの定義コードが変わりました。1.2 から 1.2.1 でも変わっていましたので、根幹部分の定義が立て続けに更新されている訳です。
1.2.2の変更によって(18行のコメントにもあるように)新たに jQuery オブジェクト(その実体は関数)は そのプロパティである init() メソッドをコンストラクタとするインスタンスを受け取るものとして位置づけられました。
17: var jQuery = window.jQuery = function( selector, context ) {
18: // The jQuery object is actually just the init constructor 'enhanced'
19: return new jQuery.prototype.init( selector, context ) :
20: };
ここで興味深いことは、ver1.2.1までと異なって、jQuery 関数オブジュクトそれ自体がコンストラクタなのではない、ということです。コンストラクタは jQuery のプロトタイプオブジェクトの init メソッドになっています。
さて、最初はこの変更されたコードの意味及び変更の意義が分かりませんでした。そこで改めて new 演算子やコンストラクタについて学び直さなければなりませんでした。分かっていたつもりなのに、結局半可通だったことを再認識させられたのです。
通常コンストラクタ、インスタンスの学習コード等では、例えば次のような例示が行われます。
var Rectangle = function (x,y){ this.x=x; this.y=y; }
Rectangle.prototype.area= function(){ return this.x * this.y };
Rectangle.prototype.outer = function (){ return 2*(this.x + this.y) };
var inst = new Rectangle(2,3);
inst.area(); // result = 6
inst.outer(); // result = 10
この例では、Rectangle コンストラクタ関数から生成されたインスタンスオブジェクト(inst)は、そのコンストラクタオブジェクト (Rectangle) に関連づけられたプロトタイプオブジェクト (Rectangle.prototype) からプロパティを継承する、と説明されます。
つまり、プロトタイプオブジェクトのプロパティである area() メソッドや outer() メソッドが inst オブジェクトに継承されるので、上記例の最後の2行のような結果が取得出来る、という説明です。
次に、Javascript第5版によれば「 new 演算子は、(1)空のオブジェクトを生成し、(2)コンストラクタ関数の prototypeプロパティをそのオブジェクトの prototype として設定し、(3)そのオブジェクトへの参照をキーワード this に代入し、(4)そのオブジェクトのメソッドとしてコンストラクタ関数を呼び出す」と説明されています。
上記の Rectangle に沿って言えば、(1)空オブジェクト(仮に obj とする)を生成し、(2)obj.prototypeを設定し (この prototype 値は Rectangle.prototype に同じなので obj.area()、obj.outer() が設定され)、(3)this = objとし、最後に (4) Rectangle.apply(this, [x,y]) を実行する、となります。
注目すべきことは、コンストラクタ関数の実行は最後に行われるのであって、その前にプロトタイププロパティがオブジェクトに設定されることです。もう一度順番を書けば
(1) obj 生成、(2) obj.prototype = Rectangle.prototype 設定
(3) this = obj 設定、(4) Rectangle.apply(this, [x,y]) 実行
となります。
次に同じ事を jQuery 定義コードに適用してみます。19行の new 演算子とそれに続く init 関数は
(1) obj生成、(2)obj.prototype = jQuery.prototype.init.prototype 設定
(3) this = obj設定、(4)jQuery.prototype.init.apply(this, [selector, context]) 実行
を行うことになります。
しかし、ここに登場する jQuery.prototype.init.prototype には何もプロパティやメソッドを定義していません。
一体、initメソッドのプロトタイププロパティはどんな内容なのでしょうか? それはどこで定義されているのでしょうか?───こんな疑問が湧いてきます。
そしてその答えを探したところ、それは 1.2.2 で新設された次の2行にありました。
525 // Give the init function the jQuery prototype for later instantiation 526 jQuery.prototype.init.prototype = jQuery.prototype;
つまり、jQuery.prototype.init 関数オブジェクトのプロトタイプは jQuery 関数オブジェクトのプロトタイプに等しくなります。
これを踏まえてjQuery定義コードの19行目を整理すると、
(1) obj(=this)作成
(2) this.prototype = jQuery.prototype.init.prototype = jQuery.prototype設定
(3) jQuery.prototype.init.apply(this, [selector, context])実行
となります。
こうして、jQuery 関数オブジェクトはコンストラクタ init 関数から return される実行結果 ( それは this であったり、様々な指定によって生成された[obj]であったり、[selector]であったりする ) を受け取ると共に、jQuery.prototype プロパティに定義された様々なメソッドやプロパティを継承することになります。
init は従来通り初期化関数として作動し、かつ jQuery.prototype に設定される様々なメソッドやプロパティは jQuery 関数オブジェクトに継承されるわけです。
1.2.1までの方法を敢えて変更した理由はどこにあるのでしょうか?「インスタンスを生成してから init() メソッドを起動し、その return を jQuery オブジェクトが受け取る」という従来の方法を変えた理由が分かりません。極めて合理的なコードだと思っていたのですが、1.2.2 のコードの方が何らかのメリットがなければそうしないでしょうから、是非とも変えた理由を探りたいのですが、力及ばず、残念ながら解明出来ていません。
この式は「循環参照」問題を引き起こすのではないか、と言うことに気がつきました。
右辺の prototype オブジェクト内には、当然 init プロパティがあり、その init プロパティは prototype プロパティがあります。つまり jQuery.prototype.init.prototype が右辺の prototype オブジェクト内に存在しています。
以下その繰り返しになります。
その結果無限にこれが繰り返され、メモリが消費されるのではないかと考えられます。
実際、或るサイトでそのことを確認したところ下図のように際限なくinit.prototype 内にinit.prototype が内包されていることが確認できました。

どこまで行っても際限なく繰り返される init.prototype 循環にコードとして問題はないのだろうか?
special オブジェクトは bind( elem ,fn ) 又は unbind( elem ,fn ) メソッドを使って、reaady、maouenter 及び mouseleave イベントタイプとそのハンドラー関数を elem に登録し、あるいはこれを解除する為に ver1.2.2 で新設されました。それは、(un)bind('ready',fn)、(un)bind('mouseenter', fn) 又は (un)bind('mouseleave', fn) の6つのインスタンスメソッドが起動された場合にだけ、jQuery.event.add() 又は jQuery.event.remove() メソッド内で呼び出されます。
special オブジェクトは ready、mouseenter 及び mouseleave の3つのプロパティを持っています。ready は setup と teardown の2つの関数オブジェクトで構成され、他の2つのプロパティは setup、teardown 及び handler の3つの関数オブジェクトから構成されています。(下図参照)
special = {
ready : { setup : fn11, teardown : fn12 },
mouseenter : { setup : fn21, teardown ; fn22, handler : fn23 },
mouseenter : { setup : fn31, teardown ; fn32, handler : fn33 }
}
ここに、それぞれの関数の役割は setup はイベントの登録用、teardown は登録済みイベント解除用、handler は各タイプのイベントに対するハンドラー関数です。
hover()メソッドは jQuery ver1.2.1 では mouseover/out イベントを扱うためのトグルメソッドでした。ところが、1.2.2 ではコードを一見すると hover()メソッドは、mouseenter/leave を扱うように変更されたように見えます。mouseover/out は hover()メソッドから切り離されてしまったような印象を受けます。
しかし、コードを詳細に見てみるとそうではないことが直ぐに分かります。hover()メソッドは IE においては mouseenter/leave イベントをサポートし、その他のブラウザにおいては mouseover/out イベントを扱うように拡張されたのです。
この拡張が何故もたらされたか、その理由は判然としませんが、過去の hover() にバグはないようですから、市場占有率が相変わらず高い IE に配慮したと理解すればよいのかも知れません。mouseenter/leave イベントが IE だけのイベントであって他のブラウザでは存在しないのに、敢えてこれらのイベントをサポートする理由は、まだ IE を無視する訳にはいかないためとしか考えられません。
関連して、それでは $(args).eventtype() 形式のイベント発動メソッドにおいて、eventtype として利用できるイベントの種類はどう変わったのか確認してみました。すると、こちらは 1.2.1 と 1.2.2 で全く同一でした。つまり、mouseenter/leave イベントは hover() メソッドだけで利用可能なのに対して、mouseover/out イベントは hover() メソッドでの利用だけではなく、それぞれを単独で利用することも想定した仕様となっています。
勿論、bind メソッドを使って mouseenter/leave イベントを単独使用することは可能でしょうが、IE だけでしか動かないコードを作る意味はありません。
specialオブジェクトは、jQuery.evemt.add() メソッド又は jQuery.evemt.remove() メソッドから呼ばれます。前者は当該イベントをバインドする際に、後者はそのイベントの登録を解除する時に、です。
2126: special: {
2127: ready: {
2128: setup: function() {
2129: // Make sure the ready event is setup
2130: bindReady();
2131: return;
2132: },
2133:
2134: teardown: function() { return; }
2135: },
2136:
これは単に未定義値を return するだけです。何もしません。呼出し元に戻った際には return により返された undefined は !== false ですから、1875-1878 行の Listner 等のバインダー関数は起動されません。
2137: mouseenter: {
2138: setup: function() {
2139: if ( jQuery.browser.msie ) return false;
2140: jQuery(this).bind("mouseover", jQuery.event.special.mouseenter.handler);
2141: return true;
2142: },
2143:
2144: teardown: function() {
2145: if ( jQuery.browser.msie ) return false;
2146: jQuery(this).unbind("mouseover", jQuery.event.special.mouseenter.handler);
2147: return true;
2148: },
2149:
2150: handler: function(event) {
2151: // If we actually just moused on to a sub-element, ignore it
2152: if ( withinElement(event, this) ) return true;
2153: // Execute the right handlers by setting the event type to mouseenter
2154: arguments[0].type = "mouseenter";
2155: return jQuery.event.handle.apply(this, arguments);
2156: }
2157: },
2158:
※ この関数は IE からは呼び出されません。
上の説明ではもちろんのこと、special["type"].setup()メソッドは大変分かりにくいので、IE 以外のブラウザで mouseenter イベントをバインドする時の、スクリプトの進行過程を辿るフローチャート的資料を作ってみましたので、以下に掲載します。
左図をクリックすると原寸画像が別窓で開きます。mouseenter イベントタイプ名から、どのようにして mouseover イベントの登録に至るのか、その経過を分かりやすく説明したつもりです。
2159: mouseleave: {
2160: setup: function() {
2161: if ( jQuery.browser.msie ) return false;
2162: jQuery(this).bind("mouseout", jQuery.event.special.mouseleave.handler);
2163: return true;
2164: },
2165:
2166: teardown: function() {
2167: if ( jQuery.browser.msie ) return false;
2168: jQuery(this).unbind("mouseout", jQuery.event.special.mouseleave.handler);
2169: return true;
2170: },
2171:
2172: handler: function(event) {
2173: // If we actually just moused on to a sub-element, ignore it
2174: if ( withinElement(event, this) ) return true;
2175: // Execute the right handlers by setting the event type to mouseleave
2176: arguments[0].type = "mouseleave";
2177: return jQuery.event.handle.apply(this, arguments);
2178: }
2179: }
2180: }
mouseleave が mouseout に、jQuery.event.special.mouseenter.handler が jQuery.event.special.mouseleave.handler に変わる以外は、上の mouseenter の場合と全く同様ですので記述を省略します。
上に見たように、special オブジェクトはまさに「 特別に 」難解です。
特に type 名が mouseenter/leaveの場合の setup/teardown メソッドは、bind メソッドと add メソッドの再帰呼び出しを含み、しかも途中でイベント名を変更しつつ、ブラウザ毎に不必要なイベントは登録しないという「 裏技的 」技法を用いて mouseover/out イベントにも同時対応させており、難解を極めます。
このエントリイ作成に当たり、沢山の時間を要してコード進行を図示したり、何度も FireBug で追跡したり、色々チャレンジしてやっと「 理解できた 」のですが、この一連の Event 関連コード解読作業において、この箇所が最も苦労させられました。
上の記述でこれらの挙動を的確に表現できた自信はありませんし、読むだけで分かる表現になっているとも思えません。(書いておきながら、失礼なことですが...)
本来、もっと分かりやすい説明・解読文章を書ければ、と悔やまれます。
jQuery.js の 1.2.2 登場を踏まえて、急遽イベント関連エントリイの全てを 1.2.2 対応に改訂しました。
Ver1.2.2によって、イベント関連コードにおいては、複数イベントタイプの一括登録、bindReady() メソッドの大幅改訂、IE 固有の mouseenter/leave イベントへの対応、テキストノードとコメントノードが引数になった場合のエラー防止コードの挿入、extra 関数に係る微修正、ページ座標に関する微修正、随所における IE メモリリーク対応の追加、等々の改訂が行われました。
これらの改訂の全てについて論じることは出来ませんでしたが、改訂エントリイにおいてその大半に触れることが出来ました。
よろしければ上の目次を参考に、任意のコンテンツをご覧いただければ幸いです。
slideToggleメソッドの活用について、以前に詳細に記しましたが、コードの全面的な改定を行いましたので、改めてそのエントリイを改訂し、全面改定したコードについて纏め直しました。
よろしければ見てやってください。→ jQuery()活用(1) Slide Toggle Button──jQuery解読(15)
改訂したのはこれまでのコードにバグがあったためではなく、使い勝手が悪かったからです。特殊な状況でしか使えないコードだったので汎用化を図りました。具体的な改訂内容は以下の通りです。
ボタンクリックによる slideToggle は、利活用が容易でなければ多用出来ません。ですから汎用的であるほどに意味があると思います。
今回の改訂がその目的・機能を果たしきれているかどうか、自信はありませんが、一応の目的を果たせたと思っています。
1.2.2が公開された。いや、公開されていた。
数日前に未公開の(正確に言えばダウンロードページには公開されていない)1.2.2bに触れたが、この時に気がつくべきだった。
b があれば当然その前に a があるはずであり、更にその前には、添え字のない1.2.2があるはずだ───と。
さて、最も大きな変更はグローバル変数 jQuery の定義が変わったことだろう。
従来の再帰呼び出し的定義は廃止され、「純粋に」コンストラクタとして位置づけられたのだ。
次に、本家の note頁 で強調されている jQuery(DOMElement) 呼び出しの Speedup は、単項引数でそれがDOM Elementであった場合のコードが追加され、この結果としてもたらされたものである。
なお、jQueryのコンストラクタへの純化による全体への影響がどうなるのか、近いうちに検証してみたい。
また、直近で解読してきた event 関連の若干の追加修正もあったし、細かなバグ修正も120箇所以上に及ぶようだ。
現在進めている解読作業の対象バージョンを、随時過去に遡って 1.2.2 に変更していこう、と思っている。
既に内容的に簡単なエントリイの改訂作業は終えた。
可及的速やかにイベント関連コード解読の全体を1.2.2対応へとバージョンアップしようと思う。
テスト
$(document).ready(fn) が bindReady() を、そしてそこから $.ready() が呼び出される三層構造で、これらのメソッドが機能します。
それぞれの役割は、順に以下のようになります。
以下に順にこれらのメソッドや関数の挙動を解読します。
※ この時点で DOM 読み込みが完了していない状態とは、bindReady()メソッドが起動されて DOMContentLoaded イベントの発生(またはそれに準じる状態)を待機する状態になり、その後 jQuery.ready() メソッドも起動し終えたものの、まだ DOM 読み込み完了が終わらない期間に発生します。
DOM ツリーが巨大なほどこの待機時間は長くなる訳です。
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 }
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 < document.styleSheets.length; 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:
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: }このイベントは Mozilla 系と Opera には実装され、IE とsafari は未実装です。
こちら 第30回 JavaScriptの動作を軽くするための工夫:ITpro が大変参考になります。
イベントの登録メソッドに較べて、それを削除する $(args).unbind() は余り使用頻度の高いメソッドではないでしょう。登録したイベントハンドラーをわざわざ解除する必要性は、決して高いとは思えないからです。
そのことはさて置き、$(args).unbind() メソッドは僅か数行のもので、全ての処理を $.event.remove() メソッドに委ねており、更に $.event.remove() メソッドの骨格は $(args).data() と $(args).removeData() メソッドによって構成されています。
ここでも $(args).add() メソッド同様に、本家サイトで Internals ジャンルに分類されている Data Cache を取り扱うメソッドが不可欠となっています。
$(args).unbind() のコードは以下の通りです。対象要素ノード毎に $.event.remove() を呼び出してその結果を受け取っています。
2199: unbind: function( type, fn ) {
2200: return this.each(function(){
2201: jQuery.event.remove( this, type, fn );
2202: });
2203: },
unbind() メソッドから3つの引数を受け取り、実際にイベントをアンバインドするメソッドです。
elem がテキストノードかコメントだった場合には何もせずに終わります。
1902 行の data() メソッドにより、当該対象要素ノードに対応する Namespaced event handlers をData Cache から抽出し、変数 events に代入します。なお、変数を3つ指定していますが、index はどこでも使われていません。
1896: // Detach an event or set of events from an element
1897: remove: function(elem, type, handler) {
1898: // don't do events on text and comment nodes
1899: if ( elem.nodeType == 3 || elem.nodeType == 8 )
1900: return;
1901:
1902: var events = jQuery.data(elem, "events"), ret, index;
1903:
ここで、events 変数には何が代入されるのか、視覚的にも明らかにしておくことが得策だと思います。
下図はあるサイトのある要素ノードに、クリックとダブルクリックの2つのイベントハンドラーをバインドした時の、Namespaced events handlers と jQuery.js が呼んでいるオブジェクトの一部、つまり jQuery.cache オブジェクトの一部を切り取ったものです。
この図にある events オブジェクトと同じような構造の、「 element 」要素ノードに対応するオブジェクトが 1651 行の変数 events に代入されます。
// 当該対象ノードにイベントハンドラーが登録されていれば
1904: if ( events ) {
/* 削除その1
* 第2引数の type が与えられていない場合の処理である。
* この場合には、events オブジェクト内の全ての type プロパティ、つまり登録
* されている全てのイベントハンドラー毎に、remove() メソッドを再帰呼び出し
* する。再帰的に呼び出す場合には、events オブジェクトから type 名称を取り
* だして第2引数としている。
* このブロックの処理によって、対象要素ノードに登録されている Namespaced
* イベントハンドラーの全てを削除することになる。
*/
1905: // Unbind all events for the element
1906: if ( types == undefined )
1907: for ( var type in events )
1908: this.remove( elem, type );
/* イベントオブジェクトが引数となった場合の処理
* 「type.type がある場合」とは、何らかの事情によって第2引数の type が
* 文字列ではなく、イベントオブジェクトであった場合を指している。
* その場合には、handler にはイベントオブジェクトの handler プロパティを
* type にはイベントオブジェクトのtypeプロパティを代入している。
*/
1909: else {
1910: // types is actually an event object here
1911: if ( types.type ) {
1912: handler = types.handler;
1913: types = types.type;
1914: }
/* の処理
* 複数イベント一括 bind() メソッド に呼応して、unbind()メソッドもマルチ
* イベント対応としている。
* つまり、各イベントタイプ毎にイテレートして登録解除を進めることになる。
* type にはイベントtype名が入る。
*/
1916: // Handle multiple events seperated by a space
1917: // jQuery(...).unbind("mouseover mouseout", fn);
1918: jQuery.each(types.split(/\s+/), function(index, type){
1919: // Namespaced event handlers
1920: var parts = type.split(".");
1921: type = parts[0];
/* 削除その2 * 次は第2引数の type が与えられていて、それが events オブジェクトの * プロパティと一致する場合── if ( events[type] ) ──の処理である。 * 1925行……第3引数 handler が与えられていれば、 * 1926行……登録されているプロパティ=ハンドラー関数を削除する。 * ここでは type 名のオブジェクトは削除せず、その handler 名の関数 * だけを削除する。 * ここに handler.guid プロパティの値はハンドラー関数である。 * ※ event.add() メソッド参照。 */ 1923: if ( events[type] ) { 1924: // remove the given handler for the given type 1925: if ( handler ) 1926: delete events[type][handler.guid]; 1927:
* 削除その3 * 第3引数の handler が与えられていない場合には(1929行以下) * events[type] オブジェクト内の各プロパティに対して(1930行) * カスタムイベントがないか(!parts[1])、又は登録されているカスタムイベ * ントタイプと、指定されたカスタムイベントタイプが一致すれば(1932後段)、 * handler プロパティを削除する。 * これにより指定した type で登録されているイベントハンドラー関数を * 全て削除する。 */ 1928: // remove all handlers for the given type 1929: else 1930: for ( handler in events[type] ) 1931: // Handle the removal of namespaced events 1932: if ( !parts[1] || events[type][handler].type == parts[1] ) 1933: delete events[type][handler]; 1934:
/* event[type] オブジェクト内にプロパティが残ってないかチェックする。 * もし、残っていれば for loop を 中止する。 */ 1935: // remove generic event handler if no more handlers exist 1936: for ( ret in events[type] ) break;
/* 削除その4 * もし、 event[type] オブジェクト内にプロパティが残っていれば、 * !ret は false となるので 1938行以下は実行されない。 * さて、もはや eventsオブジェクトに type プロパティが存在しない場合 * ──if ( !ret )──には、一般的なイベントハンドリングによって登録した * ハンドラー関数の登録を解除する。 * ここに jQuery.data(element, "handle") はイベントハンドラー登録時に利用 * される代理母的な関数である。既に add() 及び handle() メソッドの項で述べ * たように、add()メソッドによってNamespaced イベントハンドラーに登録済みの * 関数から、jQuery.event.handle() メソッドがバインドする関数を選択し登録する。 */ 1937: if ( !ret ) { 1938: if ( !jQuery.event.special[type] || jQuery.event.special[type].teardown.call(elem) === false ) { 1939: if (elem.removeEventListener) 1940: elem.removeEventListener(type, jQuery.data(elem, "handle"), false); 1941: else if (elem.detachEvent) 1942: elem.detachEvent("on" + type, jQuery.data(elem, "handle")); 1943: } 1944: ret = null; 1945: delete events[type]; 1946: } 1947: } 1948: }); 1949: }
/* 削除その5 * 最後の処理は再び Data Cache オブジェクト内のクリーニングである。 * これまでの処理で全てのイベントハンドラー(タイプ、カスタムイベント、 * ハンドラー関数、id 番号など)を削除するが、まだ Data Cache オブジェク * ト内には残存物がある。それは events と handle オブジェクトである。 * これらを以下の数行で削除する。 * ret がないことつまり events オブジェクト内が空であることを確認して、 * events と handle オブジェクトを削除する。 * 以上の結果、当該要素ノードelementに関してData Cache オブジェクト内に * 存在するオブジェクトは、当該要素ノードに対応するuuid番号を振られた空 * のオブジェクトだけとなる。 */ 1951: // Remove the expando if it's no longer used 1952: for ( ret in events ) break; 1953: if ( !ret ) { 1954: var handle = jQuery.data( elem, "handle" ); 1955: if ( handle ) handle.elem = null; 1956: jQuery.removeData( elem, "events" ); 1957: jQuery.removeData( elem, "handle" ); 1958: } 1959: } 1960: },
jQuery.removeData() メソッドは、jQuery.data()メソッドで構築した Data Cache オブジェクト内のオブジェクトを削除するためのもので、jQuery.event.remove()メソッドの最後に登場します。その働きは既に上で述べたので、ここでは当該メソッドの解読を行ってこのエントリイを閉めます。
コード全体は name 引数がある場合とない場合の2通りに大別されます。
668: removeData: function( elem, name ) {
669: elem = elem == window ?
670: windowData :
671: elem;
672:
/* data() メソッドで elem, name を登録する時に、elemには、expando
* プロパティが付与される。
* ( data() メソッドの 518行 id = elem[ expando ] = ++uuid;)
* ここではこれを取りだして改めて地域変数 id に代入している。
* なお、この id は先の 518 行により、個々の elem に対応して
* cache オブジェクト内のプロパティとして割り振られているので、
* cache[id]とすれば 当該 uuid番号 のプロパティ値を指すことになる。
* 勿論 elem の指定ミスがあればこの行は通過し、540行でも撥ねられ、
* その先に進んでも全てスルーする。
* また name がないか間違っている場合も全ての行がスルーされる。
*/
673: var id = elem[ expando ];
674:
/* 抽出された id 番号を頼りに、その中にある name プロパティを削除する。
* 1697、1698 行の引数に置き換えれば events と handle を削除することに
* なります。
*/
675: // If we want to remove a specific section of the element's data
676: if ( name ) {
677: if ( jQuery.cache[ id ] ) {
678: // Remove the section of cache data
679: delete jQuery.cache[ id ][ name ];
680:
/* id オブジェクト内に何もないことを確認し(546行)、 * なければ(547行)、elem つまり当該 elementに対応する id 番号名のオブ * ジェクトそのものを、removeData()を再帰呼び出しして削除する。(548行) * なお 548 行を 564 行と同様のコードとしなかったのは、別の name 名のオブ * ジェクトが残っているケースもあるため、これに配慮したものと推測される。 * 例えば、events オブジェクトを削除する段階では、まだ handle オブジェクト * が残っている。(1697-1698行参照) */ 681: // If we've removed all the data, remove the element's cache 682: name = ""; 683: 684: for ( name in jQuery.cache[ id ] ) 685: break; 686: if ( !name ) 687: jQuery.removeData( elem ); 688: } 689:
/* name 引数が与えられなかった場合の処置
* elem の expando プロパティ削除を行う(556行)。
* IE ではエラーが発生するのでDOMの removeAttribute メソッドを使って
* elem の expando プロパティを削除する。
* 最後に cache オブジェクト内の id プロパティを削除し、elem に対応する
* cache オブジェクト内の全ての痕跡を消し去る。
*/
691: // Otherwise, we want to remove all of the element's data
692: } else {
693: // Clean up the element expando
694: try {
695: delete elem[ expando ];
696: } catch(e){
697: // IE has trouble directly removing the expando
698: // but it's ok with using removeAttribute
699: if ( elem.removeAttribute )
700: elem.removeAttribute( expando );
701: }
702:
703: // Completely remove the data cache
704: delete jQuery.cache[ id ];
705: }
706: },
jQuery() の挙動を解読する(3) jQuery.extend()及びjQuery.prototype.extend()──jQuery解読(7) において、jQuery.extend() メソッドの deep copy 部分にバグがあることを示唆しました。
そのバグは通常のメソッド拡張やオブジェクト拡張の場合には出現しないものなので、直ぐに発覚しなかったのでしょう。また、そもそも deep copy を利用するユーザーが余り多いとは思えないので、その点からも発覚しにくかったものと思われます。
さて、別件で本家サイトを閲覧していて、「ところでこのサイトは、どの版の jQuery.js を利用しているのだろうか?」とふと疑問に思い、本家サイトが利用している版を調べました。
すると、それは公開されている最新版である 1.2.1 ではなく、1.2.2b2 という版でしたので早速ゲットし、jQuery.extend()メソッドに変更が加えられてないかどうか確認してみました。
1.2.2b 版の jQuery.extend() メソッドは、利用変数名が大幅に変更され、かつ指摘したバグ箇所には修正が施されていました。
修正箇所の詳細は別途述べるとして、参考のために 1.2.2b2 版の jQuery.extend() メソッドを以下に掲載しておきます。
なお、ここで触れている1.2.2b2 版は Date: 2007-12-20 10:14:13 -0500 付けのものです。直近のnightly 版は同じ1.2.2b2 ですが、日付は Date: 2008-01-14 14:17:35 -0500 となっています。
jQuery.extend = jQuery.fn.extend = function() {
// copy reference to target object
var target = arguments[0] || {}, i = 1, length = arguments.length, deep = false, options;
// Handle a deep copy situation
if ( target.constructor == Boolean ) {
deep = target;
target = arguments[1] || {};
// skip the boolean and the target
i = 2;
}
// Handle case when target is a string or something (possible in deep copy)
if ( typeof target != "object" && typeof target != "function" )
target = {};
// extend jQuery itself if only one argument is passed
if ( length == 1 ) {
target = this;
i = 0;
}
for ( ; i < length; i++ )
// Only deal with non-null/undefined values
if ( (options = arguments[ i ]) != null )
// Extend the base object
for ( var name in options ) {
// Prevent never-ending loop
if ( target === options[ name ] )
continue;
// Recurse if we're merging object values
if ( deep && options[ name ] && typeof options[ name ] == "object" && target[ name ] && !options[ name ].nodeType )
target[ name ] = jQuery.extend( target[ name ], options[ name ] );
// Don't bring in undefined values
else if ( options[ name ] != undefined )
target[ name ] = options[ name ];
}
// Return the modified object
return target;
};
これらのメソッドに該当するコードは jQuery.js ver1.2.2 の 2205-2215 行です。2214 行だけが1.2.1とことなる箇所で新規追加行になります。バグフィックスと言うところでしょう。
2205: trigger: function( type, data, fn ) {
2206: return this.each(function(){
2207: jQuery.event.trigger( type, data, this, true, fn );
2208: });
2209: },
2210:
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: },
これらのインスタンスメソッドは、全く同様の引数をとり、共に対象要素に登録されている、type で指定されたイベントタイプに対応するイベントハンドラー関数を起動します。しかし、微妙に異なる引数(上のピンク色の2箇所)を $.event.trigger() に渡してこのクラスメソッドを起動し、このメソッドに全ての処理を委ねた上で処理結果を return します。したがってこれらの2つのインスタンスメソッドの挙動を知るには $.event.trigger メソッドを理解する必要があります。
しかしその前に、2つのメソッドの差異を外観しておきます。
更に、$.event.trigger() クラスメソッドの解読を行う前に、これらのインスタンスメソッドの実例を見ておきます。これらのメソッドが何を行うのか理解することが先決であり、本家 jQuery サイトにいくつかの例示がありますが、第三引数の fn を与えた例はなく、また例そのものが分かりやすいとは言えないからです。
例は、$(args).trigger() メソッドの引数が1つだけ、2つ及び3つの場合の各々の違いが分かるように3パターン作成し、更にtriggerされるイベントタイプも click、dblclick 及び mouseover の3種類とし、合計9つのパターンの例を作成しました。
複雑な挙動をするtrigger()インスタンスメソッドを十分に理解するには、これだけの例が必要でした。
使い方
triggerHandler() メソッドは、後に $.event.trigger() メソッドの解読でその仕組みを明らかにしますが、trigger メソッドと異なるのは、ブラウザ既定の動作を停止させる点です。
ですから、既定の動作を持たないタイプのイベントがバインドされた要素ノードを対象として、このメソッドを適用しても何も意味がありません。
ここに既定の動作を持つイベントタイプには何があるのか、その点は不明です。分かっていることはfocus()、blur()、click()などの、a タグやinput、textarea タグなどが元々有しているメソッド名と一致するイベントタイプが「ブラウザ既定の動作を有する」ということが出来るであろうことです。
さて、実例を作ってみようとしたのですが、jQuery本家サイト以上のそれは作れませんでしたので、本家サイトの例を引用して解説しようと思います。
愈々、trigger() 及び triggerHandler() がどのようにして作用しているのか、その挙動を探ります。これらの2つのメソッドから呼び出される $.event.trigger() を解読します。
trigger() クラスメソッドは type、data、elem、donative 及び extra の5つの引数を取ります。
これらは順に、
を意味しています。
data 引数を配列にし、data がなければ空の配列を作り data に代入する。data には trigger メソッドを起動させたイベントオブジェクトなどを記述する。
1962: trigger: function(type, data, elem, donative, extra) {
1963: // Clone the incoming data, if any
1964: data = jQuery.makeArray(data || []);
1965:
第3引数の elem がない場合の処理です。この場合において、当該頁にイベントが登録されていれば、全てのノードを対象としてtrigger()メソッドを再帰呼び出しし、イベントが全く登録されていなければ何もしません。
1966: // Handle a global trigger
1967: if ( !elem ) { //elem が指定されていなければ
1968: // Only trigger if we've ever bound an event for it
1969: if ( this.global[type] ) //typeイベントが登録されていれば
// 全てのノードを対象として trigger クラスメソッドを再帰呼び出しする。
1970: jQuery("*").add([window, document]).trigger(type, data);
1971:
1972: // Handle triggering a single element
// 1の要素ノードを trigger 対象とする。
1973: } else {
/* elem がテキストやコメントだった場合
* 何もせずに undefined を返す。
*/
1974: // don't do events on text and comment nodes
1975: if ( elem.nodeType == 3 || elem.nodeType == 8 )
1976: return undefined;
1977:
/* elem[type] が関数ならば fn = true、違えば fn = false とする。
* この elem[type] は突然登場し、このメソッド以外では使われていない。
* これはブラウザ固有の type メソッドが elem に指定されている場合に
* 部分であり、2010-2017 行に連動する。
*/
1978: var val, ret, fn = jQuery.isFunction( elem[ type ] || null ),
/* data 配列が空ならば、あるいは data 配列の最初の要素に preventDefault
* プロパティがなければ(つまりそれがイベントオブジェクトでなければ)
* evt = true とする。その他の場合には evt = false とする。
* こうすることにより、イベントオブジェクトが第一引数にない場合において
* var evt = true としてダミーイベントを用意する準備をしている。
*/
1979: // Check to see if we need to provide a fake event, or not
1980: evt = !data[0] || !data[0].preventDefault;
1981:
// ダミーのイベントオブジェクトを作って data 配列の先頭要素とする。
// その type プロパティは type、target プロパティは elem とする。
1982: // Pass along a fake event
1983: if ( evt )
1984: data.unshift( this.fix({ type: type, target: elem }) );
1985:
/* data の最初の要素としてユーザーが指示したイベントオブジェクト、
* 又は 1721 行で作ったダミーのイベントオブジェクトの、type
* プロパティを type とする。
* これは専らダミーイベントへの対応策であって、イベントオブジェクトの
* type プロパティを与える目的のための行である。そうしないとダミーイベ
* ントとして用を為さないから。
*/
1986: // Enforce the right trigger type
1987: data[0].type = type;
1988:
elem に対応する cache.id オブジェクトの "handle" プロパティ値が存在し、それが関数であれば、data を引数とする関数を elem のメソッドとして実行する。ここに data[0] はこれまでの処理でイベントオブジェクトとなっている。
これにより指定した type イベントに対応して登録されているイベントハンドラー関数が呼び出され、実行される。なお、当該関数の返値がvalに代入される。
1989: // Trigger the event 1990: if ( jQuery.isFunction( jQuery.data(elem, "handle") ) ) 1991: val = jQuery.data(elem, "handle").apply( elem, data ); 1992:
(1)fn が存在せず、(2)elemに "on"+type 属性があり、(3)その属性値である関数を data を引数として実行して return 値が false ならばval に false を代入する。
これにより"on"+type 属性値である関数が励起・実行される。しかし、この行の起動を確認することは出来なかった。
1993: // Handle triggering native .onfoo handlers 1994: if ( !fn && elem["on"+type] && elem["on"+type].apply( elem, data ) === false ) 1995: val = false; 1996:
// まずダミーイベントが設定されていれば、それを data 配列から削除する。
1997: // Extra functions don't get the custom event object
1998: if ( evt )
1999: data.shift();
2000:
/* 次の部分は ver 1.2.2で大きく変わった箇所です。
* extra が既述されていてそれが関数の場合に、 val の値があれば data 配列に
* その値を結合し、なければ data のままで、その配列を引数として当該関数を実
* 行し、retに実行結果を返す。
* この場合、1998-1999 行によりダミーイベントは削除されている一方、最初に
* data にイベントオブジェクトが記述されていれば、それが引数として使われる。
*/
2001: // Handle triggering of extra function
2002: if ( extra && jQuery.isFunction( extra ) ) {
2003: // call the extra function and tack the current return value on the end for possible inspection
2004: ret = extra.apply( elem, val == null ? data : data.concat( val ) );
/* もし何かが返されれば、それを優先的に val に代入してvalを上書きする。
2005: // if anything is returned, give it precedence and have it overwrite the previous value
2006: if (ret !== undefined)
2007: val = ret;
2008: }
(1)fn が存在し、(2)donative が trueで、(3)val も true で、(4)elem が a 要素でなく、(5)type が click でないならば、triggerd の値を true としてから、elem[type]()メソッドを実行する。
この部分は donative に関わる部分、つまりブラウザ固有の動きをさせるか、させないかを決める部分である。
trigger() インスタンスメソッドの場合には donative = true として jQuery.event.trigger() メソッドが起動されるので、elem[type]() が実行される。しかし、triggerHandler() メソッドの場合には donative = false として jQuery.event.trigger() メソッドが起動されるため、ブラウザ固有のメソッドすなわち elem[ type ]() は起動されない。
2010: // Trigger the native events (except for clicks on links)
2011: if ( fn && donative !== false && val !== false && !(jQuery.nodeName(elem, 'a') && type == "click") ) {
//次の行は1843行(in add() クラスメソッド)に関わる。
2012: this.triggered = true;
2013: try {
2014: elem[ type ]();
2015 // prevent IE from throwing an error for some hidden elements
2016 } catch (e) {}
2017: }
2018:
2019: this.triggered = false;
2020: }
2021: 2022: return val; 2023: }, 2024: