1. 程式人生 > >解密jQuery事件核心

解密jQuery事件核心

說起jQuery的事件,不得不提一下Dean Edwards大神 addEvent庫,很多流行的類庫的基本思想從他那兒借來的

jQuery的事件處理機制吸取了JavaScript專家Dean Edwards編寫的事件處理函式的精華,使得jQuery處理事件繫結的時候相當的可靠。

在預留退路(graceful degradation),循序漸進以及非入侵式程式設計思想方面,jQuery也做的非常不錯

事件的流程圖

image

總的來說對於JQuery的事件繫結

在繫結的時候做了包裝處理

在執行的時候有過濾器處理

.on( events [, selector ] [, data ], handler(eventObject) )

events:事件名

selector : 一個選擇器字串,用於過濾出被選中的元素中能觸發事件的後代元素

data :當一個事件被觸發時,要傳遞給事件處理函式的

handler:事件被觸發時,執行的函式

例如:

var body = $('body')
body.on('click','p',function(){
    console.log(this)
})

用on方法給body上繫結一個click事件,冒泡到p元素的時候才出發回調函式

這裡大家需要明確一點:每次在body上點選其實都會觸發事件,但是隻目標為p元素的情況下才會觸發回撥handler

通過原始碼不難發現,on方法實質只完成一些引數調整的工作,而實際負責事件繫結的是其內部jQuery.event.add方法

on: function( types, selector, data, fn, /*INTERNAL*/ one ) {
            var origFn, type;

            // Types can be a map of types/handlers
            if ( typeof types === "object" ) {
                // ( types-Object, selector, data )
                if ( typeof selector !== "string" ) {
                    // ( types-Object, data )
                    data = data || selector;
                    selector = undefined;
                }
                for ( type in types ) {
                    this.on( type, selector, data, types[ type ], one );
                }
                return this;
            }

            if ( data == null && fn == null ) {
                // ( types, fn )
                fn = selector;
                data = selector = undefined;
            } else if ( fn == null ) {
                if ( typeof selector === "string" ) {
                    // ( types, selector, fn )
                    fn = data;
                    data = undefined;
                } else {
                    // ( types, data, fn )
                    fn = data;
                    data = selector;
                    selector = undefined;
                }
            }
            if ( fn === false ) {
                fn = returnFalse;
            } else if ( !fn ) {
                return this;
            }

            if ( one === 1 ) {
                origFn = fn;
                fn = function( event ) {
                    // Can use an empty set, since event contains the info
                    jQuery().off( event );
                    return origFn.apply( this, arguments );
                };
                // Use same guid so caller can remove using origFn
                fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );
            }
            return this.each( function() {
                jQuery.event.add( this, types, fn, data, selector );
            });

針對事件處理,我們可以拆分2部分:

一個事件預繫結期

一個事件執行期

本章著重講解事件的預繫結的時候做了那些處理,為什麼要這樣處理?

事件底層的繫結介面無非就是用addEventListener處理的,所以我們直接定位到addEventListener下面

jQuery.event.add 中有

image

elem: 目標元素

type: 事件型別,如’click’

eventHandle: 事件控制代碼,也就是事件回撥處理的內容了

false: 冒泡

現在我們把之前的案例給套一下看看

複製程式碼

var body = document.getElementsByTagName('body')

var eventHandle = function(){
    console.log(this) 
}

body .addEventListener( ‘click’, eventHandle, false );

複製程式碼

明顯有問題,每次在body上都觸發了回撥,少了個p元素的處理,當然這樣的效果也無法處理

eventHandle原始碼

回到內部繫結的事件控制代碼eventHandle ,可想而知eventHandle不僅僅只是隻是充當一個回撥函式的角色,而是一個實現了EventListener介面的物件

複製程式碼

if ( !(eventHandle = elemData.handle) ) {
    eventHandle = elemData.handle = function( e ) {
        // Discard the second event of a jQuery.event.trigger() and
        // when an event is called after a page has unloaded
        return typeof jQuery !== core_strundefined && (!e || jQuery.event.triggered !== e.type) ?
            jQuery.event.dispatch.apply( eventHandle.elem, arguments ) :
            undefined;
    };
    // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events
    eventHandle.elem = elem;
}

複製程式碼

可見在eventHandle中並沒有直接處理回撥函式,而是對映到jQuery.event.dispatch分派事件處理函數了

僅僅只是傳入eventHandle.elem,arguments , 就是body元素 與事件物件

那麼這裡有個問題,事件回撥的控制代碼並沒有傳遞過去,後面的程式碼如何關聯?

本章的一些地方可能要結合後面的dispatch處理才能理清,但是我們還是先看看做了那些處理

on內部的實現機制

我們開從頭來理清下jQuery.event.add程式碼結構,適當的跳過這個環節中不能理解的程式碼,具體遇到在提出

之前就提到過jQuery從1.2.3版本引入資料快取系統,貫穿內部,為整個體系服務,事件體系也引入了這個快取機制

所以jQuery並沒有將事件處理函式直接繫結到DOM元素上,而是通過.data儲存在快取.data儲存在快取.cahce上

第一步:獲取資料快取

//獲取資料快取
    elemData = data_priv.get( elem );

在$.cahce快取中獲取儲存的事件控制代碼物件,如果沒就新建elemData

第二步:建立編號

if ( !handler.guid ) {
                handler.guid = jQuery.guid++;
            }

image

為每一個事件的控制代碼給一個標示,新增ID的目的是 用來尋找或者刪除handler,因為這個東東是快取在快取物件上的,沒有直接跟元素節點發生關聯

第三步:分解事件名與控制代碼

複製程式碼

if ( !(events = elemData.events) ) {
    events = elemData.events= {};
}

if ( !(eventHandle = elemData.handle) ) {
    eventHandle = elemData.handle = function( e ) {
        // Discard the second event of a jQuery.event.trigger() and
        // when an event is called after a page has unloaded
        return typeof jQuery !== core_strundefined && (!e || jQuery.event.triggered !== e.type) ?
            jQuery.event.dispatch.apply( eventHandle.elem, arguments ) :
            undefined;
    };
    eventHandle.elem = elem;
}

複製程式碼

events,eventHandle 都是elemData快取物件內部的,可見

在elemData中有兩個重要的屬性,

一個是events,是jQuery內部維護的事件列隊

一個是handle,是實際繫結到elem中的事件處理函式

之後的程式碼無非就是對這2個物件的篩選,分組,填充了

第四步: 填充事件名與事件控制代碼

複製程式碼

// Handle multiple events separated by a space
// jQuery(...).bind("mouseover mouseout", fn);
// 事件可能是通過空格鍵分隔的字串,所以將其變成字串陣列
// core_rnotwhite:/\S+/g
types = ( types || "" ).match( core_rnotwhite ) || [""];
// 例如:'.a .b .c'.match(/\S+/g) → [".a", ".b", ".c"]
// 事件的個數
t = types.length;

while ( t-- ) {
    // 嘗試取出事件的名稱空間
    // 如"mouseover.a.b" → ["mouseover.a.b", "mouseover", "a.b"]
    tmp = rtypenamespace.exec( types[t] ) || [];
    // 取出事件型別,如mouseover
    type = origType = tmp[1];
    // 取出事件名稱空間,如a.b,並根據"."分隔成陣列
    namespaces = ( tmp[2] || "" ).split( "." ).sort();

    // There *must* be a type, no attaching namespace-only handlers
    if ( !type ) {
        continue;
    }

    // If event changes its type, use the special event handlers for the changed type
    // 事件是否會改變當前狀態,如果會則使用特殊事件
    special = jQuery.event.special[ type ] || {};

    // If selector defined, determine special event api type, otherwise given type
    // 根據是否已定義selector,決定使用哪個特殊事件api,如果沒有非特殊事件,則用type
    type = ( selector ? special.delegateType : special.bindType ) || type;

    // Update special based on newly reset type
    // type狀態發生改變,重新定義特殊事件
    special = jQuery.event.special[ type ] || {};

    // handleObj is passed to all event handlers
    // 這裡把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 );

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

        // Only use addEventListener if the special events handler returns false
        // 如果獲取特殊事件監聽方法失敗,則使用addEventListener進行新增事件
        if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
            if ( elem.addEventListener ) {
                elem.addEventListener( type, eventHandle, false );
            }
        }
    }

    // 特殊事件使用add處理
    if ( special.add ) {
        special.add.call( elem, handleObj );
        // 設定事件處理函式的ID
        if ( !handleObj.handler.guid ) {
            handleObj.handler.guid = handler.guid;
        }
    }

    // Add to the element's handler list, delegates in front
    // 將事件處理物件推入處理列表,姑且定義為事件處理物件包
    if ( selector ) {
        handlers.splice( handlers.delegateCount++, 0, handleObj );
    } else {
        handlers.push( handleObj );
    }

    // Keep track of which events have ever been used, for event optimization
    // 表示事件曾經使用過,用於事件優化
    jQuery.event.global[ type ] = true;
}

// Nullify elem to prevent memory leaks in IE
// 設定為null避免IE中迴圈引用導致的記憶體洩露
elem = null;
},

複製程式碼

這段比較長了分解下,最終的目的就是為填充events,eventHandle

涉及

多事件處理

如果是多事件分組的情況jQuery(...).bind("mouseover mouseout", fn);

事件可能是通過空格鍵分隔的字串,所以將其變成字串陣列

增加名稱空間處理

事件名稱可以新增指定的event namespaces(名稱空間) 來簡化刪除或觸發事件。例如,"click.myPlugin.simple"為 click 事件同時定義了兩個名稱空間 myPlugin 和 simple。通過上述方法繫結的 click 事件處理,可以用.off("click.myPlugin") 或 .off("click.simple")刪除繫結到相應元素的Click事件處理程式,而不會干擾其他繫結在該元素上的“click(點選)” 事件。名稱空間類似CSS類,因為它們是不分層次的;只需要有一個名字相匹配即可。以下劃線開頭的名字空間是供 jQuery 使用的。

引入jQuery的Special Event機制

什麼時候要用到自定義函式?有些瀏覽器並不相容某型別的事件,如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方法在刪除處理器前也會檢查此。

!special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false
jQuery.removeEvent( elem, type, elemData.handle );

jQuery.event.special物件中,儲存著為適配特定事件所需的變數和方法,

具體有: delegateType / bindType (用於事件型別的調整) setup (在某一種事件第一次繫結時呼叫) add (在事件繫結時呼叫) remove (在解除事件繫結時呼叫) teardown (在所有事件繫結都被解除時呼叫) trigger (在內部trigger事件的時候呼叫) noBubble _default handle (在實際觸發事件時呼叫) preDispatch (在實際觸發事件前呼叫) postDispatch (在實際觸發事件後呼叫)

在適配工作完成時,會產生一個handleObj物件,這個物件包含了所有在事件實際被觸發是所需的所有引數

採用自定義事件或者瀏覽器介面繫結事件

if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
                        if ( elem.addEventListener ) {
                            elem.addEventListener( type, eventHandle, false );
                        }
                    }

冒泡標記

  handlers.splice( handlers.delegateCount++, 0, handleObj );

最後記得

設定為null避免IE中迴圈引用導致的記憶體洩露

elem = null;

這個元素沒有直接讓事件直接引用了,而是掛在到,資料快取控制代碼上,很好的避免了這個IE洩露的問題

eventHandle.elem = elem;

通過整個流程,我們的資料快取物件就填充完畢了,看看截圖

events:handleObj 

image

handle

image

資料快取物件

image

得出總結:

在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內部維護的事件列隊來區分(就是那個elemData.events)

在elemData中獲取到events和handle之後,接下來就需要知道這次繫結的是什麼事件了

畫了個簡單流程圖

image

本章只是事件的前段部分,解析完elemData資料,在執行期間又會如何處理,如何巧妙的運用這些設計,下章分曉!

如果覺得有幫助,請點選下推薦,分享給更多有需要的人~

如果您看完本篇文章感覺不錯,請點選一下右下角的推薦來支援一下博主,謝謝!

如果是原創文章,轉載請註明出處!!!