jquery.jsを読み解く

第8回jQueryライブラリ(1795行目~1961行目)

今回は、jQuery.eventに関する処理の説明になります。ここしばらくは、ブラウザの差異を吸収するコードが続いていましたが、今回のエレメントに対してイベントを割り当てる処理の部分も良くできていて、参考になるような書き方がたくさん出てきます。

jQuery.event.add()

1795: /*
1796:  * A number of helper functions used for managing events.
1797:  * Many of the ideas behind this code orignated from 
1798:  * Dean Edwards' addEvent library.
1799:  */
1800: jQuery.event = {
1801:
1802:  // Bind an event to an element
1803:  // Original by Dean Edwards
1804:  add: function(elem, types, handler, data) {
1805:    if ( elem.nodeType == 3 || elem.nodeType == 8 )
1806:      return;
1807:
1808:    // For whatever reason, IE has trouble passing the window object
1809:    // around, causing it to be cloned in the process
1810:    if ( jQuery.browser.msie && elem.setInterval != undefined )
1811:      elem = window;
1812:
1813:    // Make sure that the function being executed has a unique ID
1814:    if ( !handler.guid )
1815:      handler.guid = this.guid++;
1816:      
1817:    // if data is passed, bind to handler 
1818:    if( data != undefined ) { 
1819:      // Create temporary function pointer to original handler 
1820:      var fn = handler; 
1821:
1822:      // Create unique handler function, wrapped around original handler 
1823:      handler = function() { 
1824:        // Pass arguments and context to original handler 
1825:        return fn.apply(this, arguments); 
1826:      };
1827:
1828:      // Store data in unique handler 
1829:      handler.data = data;
1830:
1831:      // Set the guid of unique handler to the same of original handler, so it can be removed 
1832:      handler.guid = fn.guid;
1833:    }
1834:

それでは、1795行目のコメント文から見ていきましょう。Dean Edwards氏のaddEventライブラリから多くのアイデアをもらっていると書かれています。IE7.jsや/packer/を公開されている方で、Dean Edwards: addEvent() - My Solutionに元になっているソースがあるので、興味がある方はこちらも合わせて読んでみてください。

1804行目からのjQuery.event.add()メソッドは、eventtype()、bind()、one() および bindReady()から呼び出され、実際にイベントを割り当てる内部処理用のメソッドです。1805行目で、引数elemに渡された要素のnodeTypeがテキストノードまたはコメントノードの場合には何もせずにreturnしています。

1810行目では、ブラウザがInternet Explorerの場合に発生する不具合対策のためにwindowオブジェクトのクローンを生成しています。

1814行目は、イベントハンドラに対してユニークなIDを生成しています。ここでいうhandlerというのは、実際に行われる処理を定義した関数のことです。

1818行目からは、引数dataが定義されている場合の処理で、イベントハンドラに対して引数dataをバインド(束縛)します。1823行目でhandlerをラップする関数を作成し、元々のhandlerに引数を渡すように再定義しています。これは、イベントの定義時ではなく、実行時のパラメータをhandlerに対してbindするためです。また、handler.dataにdataをhandler.guidに元のguidをそれぞれ設定します。

jQuery.handle

1835:    // Init the element's event structure
1836:    var events = jQuery.data(elem, "events") || jQuery.data(elem, "events", {}),
1837:      handle = jQuery.data(elem, "handle") || jQuery.data(elem, "handle", function(){
1838:        // returned undefined or false
1839:        var val;
1840:
1841:        // Handle the second event of a trigger and when
1842:        // an event is called after a page has unloaded
1843:        if ( typeof jQuery == "undefined" || jQuery.event.triggered )
1844:          return val;
1845:    
1846:        val = jQuery.event.handle.apply(arguments.callee.elem, arguments);
1847:    
1848:        return val;
1849:      });

1836行目は、events変数の定義です。既にeventsが定義されていればその値を取得、なければ初期化します。

1837行目も同様で、既にhandleが定義されていればその値を取得、なければ新たに定義します。関数の中身は、1838~1848行目にて定義されています。1839行目でundefinedやfalseを返すために、戻り値用のval変数を定義します。そして、ページのアンロード時などjQuery変数が未定義の場合やネイティブイベントが実行中でjQuery.event.triggeredがtrueの場合には、undefinedを返します。

そうでなければ、applyメソッドを利用して関数を実行します。arguments.calleeは自身を指すので、arguments.callee.elemが実行時のthisになります。

1850: // Add elem as a property of the handle function 1851: // This is to prevent a memory leak with non-native 1852: // event in IE. 1853: handle.elem = elem; 1854:

1853行目は、handleのプロパティにelemを代入しています。コメントによると、Internet Explorerでのメモリリークを防ぐためのようです。

1855:      // Handle multiple events seperated by a space
1856:      // jQuery(...).bind("mouseover mouseout", fn);
1857:      jQuery.each(types.split(/\s+/), function(index, type) {
1858:        // Namespaced event handlers
1859:        var parts = type.split(".");
1860:        type = parts[0];
1861:        handler.type = parts[1];
1862:
1863:        // Get the current list of functions bound to this event
1864:        var handlers = events[type];
1865:
1866:        // Init the event handler queue
1867:        if (!handlers) {
1868:          handlers = events[type] = {};
1869:    
1870:          // Check for a special event handler
1871:          // Only use addEventListener/attachEvent if the special
1872:          // events handler returns false
1873:          if ( !jQuery.event.special[type] || jQuery.event.special[type].setup.call(elem) === false ) {
1874:            // Bind the global event handler to the element
1875:            if (elem.addEventListener)
1876:              elem.addEventListener(type, handle, false);
1877:            else if (elem.attachEvent)
1878:              elem.attachEvent("on" + type, handle);
1879:          }
1880:        }
1881:

1857行目からは、イベントハンドラを登録する処理で、jQuery(...).bind("mouseover mouseout", fn); のようにイベント名をスペースで区切って、複数のイベントにハンドラを一括登録できます。処理としては、引数をスペースでsplitしてeach()でループしています。

1859行目は、引数type('click'などが入ります)を'.'で分割しています。これはどういうことかというと、次のようにclickイベントを割り当ててunbindするような処理で、もしclickイベントが複数割り当てられていたら、すべてのclickイベントが解除されてしまいます。

$('.class').bind('click', function(){//whatever});
$('.class').unbind('click');

これを防ぐために、jQueryでは一連のイベントに対して名前を付けておくことができるようにしています。click.namespaceのように記述することでイベントを識別可能になります。

$('.class').bind('click.namespace', function(){//}); 
$('.class').unbind('click.namespace');

1860行目でブラウザが識別できるイベント名をtypeに格納し、1861行目でnamespaceをhandler.typeに格納します。

1864行目は現在割り当てられているイベントハンドラを取得します。そして、もし既に割り当てられているイベントがあれば、初期化を行います。2126行目以降で定義されているready, mouseenter, mouseleave以外なら、addEventListener/attachEventを使って実際に要素にイベントを割り当てます。

1882:        // Add the function to the element's handler list
1883:        handlers[handler.guid] = handler;
1884:
1885:        // Keep track of which events have been used, for global triggering
1886:        jQuery.event.global[type] = true;
1887:      });
1888:    
1889:    // Nullify elem to prevent memory leaks in IE
1890:    elem = null;
1891:  },
1892:

要素のハンドラーリストにハンドラー関数を登録し、後からどのイベントが使われているかを追跡するためにglobal[type]に格納しておきます。最後にInternet Explorerのメモリリーク対策のためにelemを空にします。

jQuery.event.remove()

1893:  guid: 1,
1894:  global: {},
1895:
1896:  // Detach an event or set of events from an element
1897:  remove: function(elem, types, handler) {
1898:    // don't do events on text and comment nodes
1899:    if ( elem.nodeType == 3 || elem.nodeType == 8 )
1900:      return;
1901:

1897行目からは、イベント登録を解除する処理になります。登録時と同様にテキストノードとコメントノードに対しては、何もせずにreturnします。

1902:    var events = jQuery.data(elem, "events"), ret, index;
1903:
1904:    if ( events ) {
1905:      // Unbind all events for the element
1906:      if ( types == undefined )
1907:        for ( var type in events )
1908:          this.remove( elem, type );

1902行目は、要素に割り当てられているイベントをeventsから取得します。同時にretとindexもローカル変数として宣言しておきます。

そして、もしイベントがみつかって、イベントの種類(引数types)が指定されていなければ、remove()を再帰的に呼び出し、すべてのイベントを解除します。

1909:      else {
1910:        // types is actually an event object here
1911:        if ( types.type ) {
1912:          handler = types.handler;
1913:          types = types.type;
1914:        }
1915:        

1911行目からは、types.typeが存在する場合、つまりtypesにイベントオブジェクトが与えられた場合の処理です。handlerおよびtypesの値をそれぞれtypesイベントのhandler、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];
1922:          

jQuery.remove()においても、複数のイベントを一括して解除できるようになっています。イベント登録時と同様に、スペースで複数イベントを解除し、'.'以降の名前空間を取り出してpartsおよびtypeに格納します。

1923:          if ( events[type] ) {
1924:            // remove the given handler for the given type
1925:            if ( handler )
1926:              delete events[type][handler.guid];
1927:      
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:

1923行目からは、先ほど分割したtypeがeventsに存在する場合の処理になります。引数handlerが指定されていれば、deleteメソッドを使ってevents[type][handler.guid]オブジェクトを削除します。

handlerが指定されていなければ、events[type]に登録されている各handlerに対して、deleteメソッドを実行します。ただし、名前空間が指定されているイベント(click.namespaceように)の場合には、1932行目のif文によって該当するイベントのみを解除します。

1935:            // remove generic event handler if no more handlers exist
1936:            for ( ret in events[type] ) break;
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:      }
1950:

1936行目のfor文と1937行目のif文にて、登録されたeventハンドラを調べます。もし、残っていなければ、2126行目以降で定義されているready, mouseenter, mouseleave以外のイベントをremoveEventListener/detachEventを使って要素からイベントを解除します。

1944行目でret変数の値をnullに設定し、events[type]オブジェクトを削除してこの部分は完了です。

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:  },
1961:

1952行目のforループでeventsオブジェクトの中身を確認して、空であれば、最後にeventsとhandleのオブジェクトを削除します。

おすすめ記事

記事・ニュース一覧