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之後,接下來就需要知道這次繫結的是什麼事件了。