1. 程式人生 > >jQuery事件繫結原理(1)

jQuery事件繫結原理(1)

jQuery事件處理機制能幫我們處理那些問題?

  • 解決瀏覽器事件相容問題
  • 可以在一個事件型別上新增多個事件處理函式,可以一次新增多個事件型別的事件處理函式
  • 提供了常用事件的便捷方法
  • 支援自定義事件
  • 擴充套件了組合事件
  • 提供了統一的事件封裝、繫結、執行、銷燬機制

這裡寫圖片描述

jQuery的事件繫結有多個方法可以呼叫,以click事件來舉例:1. click方法 2. bind方法 3. delegate方法 4. on方法

    $('#foo').click(function(){ })

    $('#foo').bind('click',function(){ })

    $("foo"
).delegate("td", "click", function() { }); $("foo").on("click", "td", function() { });

我們來看一下具體實現

click方式

jQuery.each( ("blur focus focusin focusout load resize scroll unload 
click
 dblclick " +
        "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
        "change select submit keydown keypress keyup error contextmenu"
).split(" "), function( i, name ) { // 合併15種事件統一增加到jQuery.fn上 // name引數為事件名稱 jQuery.fn[ name ] = function( data, fn ) { // 內部呼叫this.on / this.trigger return arguments.length > 0 ? this.on( name, null, data, fn ) : this.trigger( name ); }; });

bind方式

    bind: function( types, data, fn ) {
        return this.on( types, null, data, fn );
    },
    unbind: function( types, fn ) {
        return this.off( types, null, fn );
    },

同樣呼叫的this.on/this.off

delegate方式

    delegate: function( selector, types, data, fn ) {
        return this.on( types, selector, data, fn );
    },
    undelegate: function( selector, types, fn ) {
        // ( namespace ) or ( selector, types [, fn] )
        return arguments.length === 1 ? this.off( selector, "**" ) : this.off( types, selector || "**", fn );
    }

同樣呼叫的this.on/this.off

one方式

    one: function(types, selector, data, fn ) {
        return this.on(types, selector, data, fn, 1 );
    },

可見以上的介面只是修改了不同的傳遞引數,最後都交給on實現的

on方式

on的呼叫方式是.on( events, selector, data, handler(eventObject)),events是事件名,selector 是一個選擇器字串,用於過濾出被選中的元素中能觸發事件的後代元素。data 是當一個事件被觸發時,要傳遞給事件處理函式的引數。handler是事件被觸發時,執行的函式。

    var body = $('body')
    body.on('click','p',function(){
        console.log(this)
    })
    //兩種寫法相同,都是事件委託,支援動態繫結(新新增的p元素也有回撥處理函式)
    body.delegate('p', 'click', function(){
        console.log(this)
    })

用on或者delegate方法給body上繫結一個click事件,當點選的是p元素的時候才觸發回撥函式。這裡使用on或者delegate都是採用事件委託(即把p元素上的點選事件處理委託給body)。
通過原始碼不難發現,on方法實質只完成一些引數調整的工作,而實際負責事件繫結的是其內部jQuery.event.add方法。

這裡寫圖片描述

針對事件處理,我們可以拆分2部分:一個事件預繫結期,一個事件執行期。事件底層的繫結介面無非就是用addEventListener處理的,所以我們直接定位到addEventListener下面

   if(elem.addEventListener){
      //採用冒泡
      elm.addEventListener(type, eventHandle, false);
   }

這裡的eventHandle顯然是重點 ,可想而知eventHandle不僅僅只是隻是充當一個回撥函式的角色,而是一個實現了EventListener介面的物件

    if ( !(eventHandle = elemData.handle) ) { //handle是實際繫結到elem中的事件處理函式
       eventHandle = elemData.handle = function( e ) {
           jQuery.event.dispatch.apply( eventHandle.elem, arguments );
    };

可見在eventHandle中並沒有直接處理回撥函式,而是轉到jQuery.event.dispatch分派事件處理函數了。當繫結的時候有 selector 的時候(也就是事件委託),add 函式處理新增事件,而事件的執行,要靠 dispatch,比如我們上面的例子,在 $('body').on('click','p',fn),我們點選body上的所有元素,會被監聽,dispatch 函式是會執行的,但是 fn 不執行,除非我們點選p元素。
那麼這裡有個問題,jQuery.event.dispatch僅僅只是傳入eventHandle.elem,arguments , 就是body元素與事件物件。事件回撥的控制代碼並沒有傳遞過去,後面的程式碼如何關聯?
我們從開頭來理清下jQuery.event.add程式碼結構,適當的跳過這個環節中不能理解的程式碼,jQuery從1.2.3版本引入資料快取系統,DOM元素和事件要建立關係,最原始的方法是在DOM元素上繫結事件。jQuery為了不破壞DOM樹結構,通過快取的方式儲存事件。所以jQuery並沒有將事件處理函式直接繫結到DOM元素上,而是通過.data儲存在快取.cache上,elemData是這個體系的核心。

    add: function( elem, types, handler, data, selector ) {
        var handleObjIn, eventHandle, tmp,
        events, t, handleObj,
        special, handlers, type, namespaces, origType,
        elemData = data_priv.get( elem ); //儲存事件控制代碼物件,elem元素的控制代碼物件

        if ( !handler.guid ) {
              handler.guid = jQuery.guid++; //為每一個事件的控制代碼給一個標示,新增id的目的是用來尋找或者刪除handler,因為這個是快取在快取物件上的,沒有直接跟元素節點發生關聯
        }

        if ( !(events = elemData.events) ) {
             events = elemData.events = {}; //events是jQuery內部維護的事件列隊
        }
        if ( !(eventHandle = elemData.handle) ) { //handle是實際繫結到elem中的事件處理函式
            eventHandle = elemData.handle = function( e ) {
            jQuery.event.dispatch.apply( eventHandle.elem, arguments );
        };
        eventHandle.elem = elem;
        //事件可能是通過空格鍵分隔的字串,所以將其變成字串陣列
        types = ( types || "" ).match( core_rnotwhite ) || [""];
        t = types.length;
        while ( t-- ) {
            // 這裡把handleObj叫做事件處理物件,擴充套件一些來著handleObjIn的屬性
            handleObj = jQuery.extend({
                type: type,
                origType: origType,
                data: data,
                handler: handler,
                guid: handler.guid,
                selector: selector,
                needsContext: selector && jQuery.expr.match.needsContext.test( selector ),
                namespace: namespaces.join(".")
            }, handleObjIn );

            // 初始化事件處理列隊,如果是第一次使用,將執行語句
            if ( !(handlers = events[ type ]) ) {
                handlers = events[ type ] = [];
                handlers.delegateCount = 0;

                if ( elem.addEventListener ) {
                     elem.addEventListener( type, eventHandle, false );
                }
            }

            // 將事件處理物件推入處理列表,姑且定義為事件處理物件包
            if ( selector ) {
                handlers.splice( handlers.delegateCount++, 0, handleObj );
            } else {
                handlers.push( handleObj );
            }
            // 表示事件曾經使用過,用於事件優化
            jQuery.event.global[ type ] = true;
        }
        // 設定為null避免IE中迴圈引用導致的記憶體洩露
        elem = null;
    }

我們來看一下截圖瞭解一下events:handleObj物件

在elemData中有兩個重要的屬性,一個是events,是jQuery內部維護的事件列隊,一個是handle,是實際繫結到elem中的事件處理函式。
這裡有一個很重要的地方,事件名稱可以新增指定的event namespaces(名稱空間) 來簡化刪除或觸發事件。例如,click.myPlugin.simple為 click 事件同時定義了兩個名稱空間 myPlugin 和 simple。通過上述方法繫結的 click 事件處理,可以用.off(“click.myPlugin”) 或 .off(“click.simple”)刪除繫結到相應元素的Click事件處理程式,而不會干擾其他繫結在該元素上的“click(點選)” 事件。名稱空間類似CSS類,因為它們是不分層次的;只需要有一個名字相匹配即可。以下劃線開頭的名字空間是供 jQuery 使用的。
什麼時候要用到自定義函式?有些瀏覽器並不相容某型別的事件,如IE6~8不支援hashchange事件,你無法通過jQuery(window).bind(‘hashchange’, callback)來繫結這個事件,這個時候你就可以通過jQuery自定義事件介面來模擬這個事件,做到跨瀏覽器相容。jQuery(elem).bind(type, callbakc)實際上是對映到 jQuery.event.add(elem, types, handler, data)這個方法,每一個型別的事件會初始化一次事件處理器,而傳入的回撥函式會以陣列的方式快取起來,當事件觸發的時候處理器將依次執行這個陣列。
jQuery.event.add方法在第一次初始化處理器的時候會檢查是否為自定義事件,如果存在則將會把控制權限交給自定義事件的事件初始化函式,同樣事件解除安裝的jQuery.event.remove方法在刪除處理器前也會檢查。

總結

jQuery.event.dispatch.apply( eventHandle.elem, arguments ) 方法中沒有傳遞迴調物件是因為回撥的控制代碼被關聯到了elemData,也就是內部資料快取中了。
不難得出jQuery的事件繫結機制:

  • jQuery對每一個elem中的每一種事件,只會繫結一次事件處理函式(繫結這個elemData.handle)
  • 而這個elemData.handle實際只做一件事,就是把event丟到jQuery內部的事件分發程式jQuery.event.dispatch.apply(eventHandle.elem, arguments )。具體這個函式可以參考下一篇博文jQuery事件繫結原理(2)
  • 而不同的事件繫結,具體是由jQuery內部維護的事件列隊來區分(就是那個elemData.events)
  • 在elemData中獲取到events和handle之後,接下來就需要知道這次繫結的是什麼事件了。