1、javaScript傳統的事件處理
給某一個元素綁定了一個點擊事件,傳入一個回調句柄處理:
element.addEventListener('click',doSomething,false);
但是如果頁面上有幾百個元素需要綁定,那需要綁定幾百次,這樣就可以知道傳統綁定方法存在的問題:
- 大量的事件綁定,性能消耗,而且還需要解綁(IE會泄漏);
- 綁定的元素必須要存在;
- 後期生成的HTML會沒有事件綁定,需要重新綁定;
- 語法過於繁雜。
關於jQuery提出的事件綁定的一些方法:bind()、live()、on()、delegate()。對應的解除事件綁定的函數分別是:unbind()、die()、off()、undelegate()。其中,live和delegate方法利用了事件委託機制,很好的解決了大量事件綁定的問題,所以先介紹事件委託的原理。
2、事件委託
DOM有個事件流的特性,也就是說我們在頁面上觸發節點的時候事件都會上下或者向上傳播,事件捕捉和事件冒泡。
DOM2.0模型將事件處理流程分爲三個階段:一、事件捕獲階段,二、事件目標階段,三、事件起泡階段。
模型如圖所示:
事件傳送可以分爲3個階段。
(1).在事件捕捉(Capturing)階段,事件將沿着DOM樹向下轉送,目標節點的每一個祖先節點,直至目標節點。例如,若用戶單擊了一個超鏈接,則該單擊事件將從document節點轉送到html元素,body元素以及包含該鏈接的p元素。在此過程中,瀏覽器都會檢測針對該事件的捕捉事件監聽器,並且運行這件事件監聽器。
(2)在目標(target)階段,瀏覽器在查找到已經指定給目標事件的事件監聽器之後,就會運行 該事件監聽器。目標節點就是觸發事件的DOM節點。例如,如果用戶單擊一個超鏈接,那麼該鏈接就是目標節點(此時的目標節點實際上是超鏈接內的文本節點)。
(3).在冒泡(Bubbling)階段,事件將沿着DOM樹向上轉送,再次逐個訪問目標元素的祖先節點到document節點。該過程中的每一步。瀏覽器都將檢測那些不是捕捉事件監聽器的事件監聽器,並執行它們。
利用事件傳播(這裏是冒泡)這個機制,就可以實現事件委託。
具體來說,事件委託就是事件目標自身不處理事件,而是把處理任務委託給其父元素或者祖先元素,甚至根元素(document)舉例子:
<ul id="myLinks">
<li id="goSomeWhere">Go somewhere</li>
<li id="doSomeThing">Do someThing</li>
<li id="sayHi">Say Hi</li>
</ul>
以上代碼包含三個點擊後會執行操作的列表項。按照傳統的JS做法,我們需要像下面這樣爲他們添加3個事件處理程序:
var item1=document.getElementById("goSomeWhere");
var item2=document.getElementById("doSomeThing");
var item3=document.getElementById("sayHi");
EvevtUtil.addHandler(item1,"click",function(event){
location.href = "http://www.xx.com";
});
EvevtUtil.addHandler(item2,"click",function(event){
document.title = "I change the document";
});
EvevtUtil.addHandler(item3,"click",function(event){
alert("hi");
});
但是,使用事件委託,只需在DOM樹中儘量高的層次上添加一個事件處理程序:
var list = document.getElementById("myLinks");
EventUtil.addHandler(list,"click",function(event){
event = EventUtil.getEvent(event);
var target = EventUtil.getTarget(event);
switch(target.id){
case: "doSomeThing"
document.title="I change the document";
break;
case: "goSomeWhere"
location.href="http://www.xx.com";
break;
case: "sayHi"
alert("Hi");
break;
}
});
使用事件委託只爲<ul>元素添加了一個onclick事件處理程序,事件目標是被單擊的列表項,他們會冒泡至父節點通過檢測ID屬性來執行對應的操作。3、方法解析
一、bind(type,[data],function(eventObject))
bind()的作用就是在選擇到的元素上綁定特定事件類型的事件處理程序,參數含義如下:
type:事件類型,如click、change、mouseover等;
data:傳入事件處理函數的參數,通過event.data取到,可選;
function:事件處理函數,可傳入event對象,但是這裏的event是jQuery封裝的event對象,與原生的event有區別。
bind的源碼:
bind: function(types,data,fn){
return this.on(types,null,data,fn);
}
//使用方式
$("#myol li").bind('click',getHtml);
bind的特點就是直接附加一個事件處理程序到元素上,有一個綁一個,在頁面的元素不會動態添加的時候使用它沒什麼,但是如果在列表中動態增加一個列表元素li,點擊它是沒有反應的,必須再bind一次。即,在bind綁定事件的時候,這些元素必須已經存在。爲了解決這個問題,可以使用live方法。
二、live(type,[data],fn) (已被棄用)
live參數與bind一樣,源碼如下:
live: function(types,data,fn){
jQuery(this.context).on(types,this.selector,data,fn);
return this;
}
//live方法並沒有將事件處理函數綁定到自己(this)身上,而是綁定到了this.context上了,即元素的限定範圍。一般元素的範圍都是document。
所以live函數將委託的事件處理程序附加到一個頁面的document
元素,從而簡化了在頁面上動態添加的內容上事件處理的使用。
例如:
$('a').live('click',function(){alert("!!")});
JQuery把alert函數綁定到$(document)元素上,並使用’click’和’a’作爲參數。任何時候只要有事件冒泡到document節點上,它就查看該事件是否是一個click事件,以及該事件的目標元素與’a’這一CSS選擇器是否匹配,如果都是的話,則執行函數。但是使用live方法還存在以下問題:
- 在調用
.live()
方法之前,jQuery 會先獲取與指定的選擇器匹配的元素,這一點對於大型文檔來說是很花費時間的。 - 不支持鏈式寫法。例如,
$("a").find(".offsite, .external").live( ... );
這樣的寫法是不合法的,並不能像期待的那樣起作用。 - 由於所有的
.live()
事件被添加到document
元素上,所以在事件被處理之前,可能會通過最長最慢的那條路徑之後才能被觸發。 - 在移動 iOS (iPhone, iPad 和 iPod Touch) 上,對於大多數元素而言,
click
事件不會冒泡到文檔 body 上,並且如果不滿足如下情況之一,就不能和.live()
方法一起使用:- 使用原生的可被點擊的元素,例如,
a
或button
,因爲這兩個元素可以冒泡到document
。 - 在
document.body
內的元素使用.on()
或.delegate()
進行綁定,因爲移動 iOS 只有在 body 內才能進行冒泡。 - 需要 click 冒泡到元素上才能應用的 CSS 樣式
cursor:pointer
(或者是父元素包含document.documentElement
)。但是依需注意的是,這樣會禁止元素上的複製/粘貼功能,並且當點擊元素時,會導致該元素被高亮顯示。
- 使用原生的可被點擊的元素,例如,
- 在事件處理中調用
event.stopPropagation()
來阻止事件處理被添加到 document 之後的節點中,是效率很低的。因爲事件已經被傳播到document
上。 .live()
方法與其它事件方法的相互影響是會令人感到驚訝的。例如,$(document).unbind("click")
會移除所有通過.live()
添加的 click 事件!
三、delegate()
爲了解決live存在的上述問題,jQuery引入了一個新方法delegate(),其把處理程序綁定到具體的元素而非document這一根上。源碼:
delegate: function(selector,types,data,fn){
return this.on(types,selector,data,fn);
}
參數多了一個selector,用來指定觸發事件的目標元素。
例如:
$('#element').delegate('a','click',function(){
alert("!!!");
});
使用click事件和'a'這一css選擇器作爲參數把alert函數綁定到了元素'#element'上。任何時候只要有事件冒泡到$(‘#element)上,它就查看該事件是否是click事件,以及該事件的目標元素是否與CCS選擇器相匹配。如果兩種檢查的結果都爲真的話,它就執行函數。四、.on( events [, selector ] [, data ], handler(eventObject) )
events:事件名
selector : 一個選擇器字符串,用於過濾出被選中的元素中能觸發事件的後代元素
data :當一個事件被觸發時,要傳遞給事件處理函數的
handler:事件被觸發時,執行的函數
所有delegate(),live(),bind()方法內部都是調用的on方法。
undelegate(),unlive(),unbind()方法內部都是調用的off方法。
on方法源碼解析:
//on方法實質只完成一些參數調整的工作,而實際負責事件綁定的是其內部jQuery.event.add方法。
on: function( types, selector, data, fn, /*INTERNAL*/ one ) {
var type, origFn;
// 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;
}
// 遍歷types對象,針對每一個屬性綁定on()方法
// 將types[type]作爲fn傳入
for ( type in types ) {
this.on( type, selector, data, types[ type ], one );
}
return this;
}
// 參數修正
// jQuery這種參數修正的方法很好
// 可以兼容多種參數形式
// 可見在靈活調用的背後做了很多處理
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傳入false時,阻止該事件的默認行爲
// function returnFalse() {return false;}
fn = returnFalse;
} else if ( !fn ) {
return this;
}
// one()調用on()
if ( one === 1 ) {
origFn = fn;
fn = function( event ) {
// Can use an empty set, since event contains the info
// 用一個空jQuery對象,這樣可以使用.off方法,
// 並且event帶有remove事件需要的信息
jQuery().off( event );
return origFn.apply( this, arguments );
};
// Use same guid so caller can remove using origFn
// 事件刪除依賴於guid
fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );
}
// 這裏調用jQuery的each方法遍歷調用on()方法的jQuery對象
// 如$('li').on(...)則遍歷每一個li傳入add()
// 推薦使用$(document).on()或者集合元素的父元素
return this.each( function() {
jQuery.event.add( this, types, fn, data, selector );
});
},
例如:
var body = $('body')
body.on('click','p',function(){
console.log(this)
})
用on方法給body上綁定一個click事件,冒泡到p元素的時候纔出發回調函數
這裏大家需要明確一點:每次在body上點擊其實都會觸發事件,但是隻目標爲p元素的情況下才會觸發回調handler
關於上述四種方法的總結:
在下列情況下,應該使用.live()或.delegate(),而不能使用.bind():
- 爲DOM中的很多元素綁定相同事件;
- 爲DOM中尚不存在的元素綁定事件;
用.bind()的代價是非常大的,它會把相同的一個事件處理程序hook到所有匹配的DOM元素上
不要再用.live()了,它已經不再被推薦了,而且還有許多問題
.delegate()會提供很好的方法來提高效率,同時我們可以添加一事件處理方法到動態添加的元素上
我們可以用.on()來代替上述的3種方法
不足點也是有的:
- 並非所有的事件都能冒泡,如load, change, submit, focus, blur
- 加大管理複雜。
- 不好模擬用戶觸發事件
4、事件體系結構
4.1 整個事件的API有:
4.2 事件結構
所有的函數添加事件都會進入jQuery.event.add函數。該函數有兩個主要功能:添加事件、附加很多事件相關信息。下章講解源碼。
用實例來說明jQuery的事件結構:
<div id="#center"></div>
<script>
function dohander(){console.log("dohander")};
function dot(){console.log("dot");}
$(document).on("click",'#center',dohander)
.on("click",'#center',dot)
.on("click",dot);
</script>
經過添加處理環節,事件添加到了元素上,而且節點對應的緩存數據也添加了相應的數據。結構如下:
elemData = jQuery._data( elem );
elemData = {
events: {
click: {//Array[3]
0: {
data: undefined/{...},
guid: 2, //處理函數的id
handler: function dohander(){…},
namespace: "",
needsContext: false,
origType: "click",
selector: "#center",//選擇器,用來區分不同事件源
type: "click"
}
1: {
data: undefined/{...},
guid: 3,
handler: function dot(){…},
namespace: "",
needsContext: false,
origType: "click",
selector: "#center",
type: "click"
}
2: {
data: undefined,
guid: 3,
handler: function dot(){…},
namespace: "",
needsContext: false,
origType: "click",
selector: undefined,
type: "click"
}
delegateCount: 2,//委託事件數量,有selector的纔是委託事件
length: 3
}
}
handle: function ( e ) {…}/*事件處理主入口*/{
elem: document//屬於handle對象的特徵
}
}
緩存結構特點:每一個函數添加guid;使用events對象存放響應事件列表,有一個總的事件處理入口handle等。
4.3 bind、delegate、on在上面已經介紹過了,下面介紹其他API。
one():
通過one()
函數綁定的事件處理函數都是一次性的,只有首次觸發事件時會執行該事件處理函數。觸發之後,jQuery就會移除當前事件綁定。
比如$("#chua").one("click",fn);爲#chua節點綁定一次性的click事件
$(document).one("click","#chua",fn);將#chua的click事件委託給document處理。
源碼:
one: function(types, selector, data, fn) {
return this.on(types, selector, data, fn, 1);
},
內部也是通過調用on方法來實現。
trigger()和triggerHandler():
trigger觸發jQuery對象所匹配的每一個元素對應type類型的事件。比如$("#chua").trigger("click");
triggeHandler只觸發jQuery對象所匹配的元素中的第一個元素對應的type類型的事件,且不會觸發事件的默認行爲。
源碼:
trigger: function(type, data) {
return this.each(function() {
jQuery.event.trigger(type, data, this);
});
},
triggerHandler: function(type, data) {
var elem = this[0];
if (elem) {
return jQuery.event.trigger(type, data, elem, true);
}
}
兩者通過調用jQuery.event.trigger()函數實現,稍後解析。
unbind():
unbind: function( types, fn ) {
return this.off( types, null, fn );
},
undelegate():
undelegate: function( selector, types, fn ) {
// ( namespace ) or ( selector, types [, fn] )
return arguments.length === 1 ? this.off( selector, "**" ) : this.off( types, selector || "**", fn );
}
它們均調用了off函數:
off: function(types, selector, fn) {
var handleObj, type;
//傳入的參數是事件且綁定了處理函數
if (types && types.preventDefault && types.handleObj) {
// ( event ) dispatched jQuery.Event
handleObj = types.handleObj;
//types.delegateTarget是事件託管對象
jQuery(types.delegateTarget).off(
//組合jQuery識別的type
handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType,
handleObj.selector,
handleObj.handler
);
return this;
}
if (typeof types === "object") {
// ( types-object [, selector] )
for (type in types) {
this.off(type, selector, types[type]);
}
return this;
}
if (selector === false || typeof selector === "function") {
// ( types [, fn] )
fn = selector;
selector = undefined;
}
if (fn === false) {
fn = returnFalse;
}
return this.each(function() {
off函數又調用了jQuery.event.remove函數,源碼分析如下:
remove: function(elem, types, handler, selector, mappedTypes) {
var j, origCount, tmp,
events, t, handleObj,
special, handlers, type, namespaces, origType,
//獲取該元素的jQuery內部數據
elemData = data_priv.hasData(elem) && data_priv.get(elem);
//如果內部數據不存在,或者內部數據沒有events域則直接返回
if (!elemData || !(events = elemData.events)) {
return;
}
//第一步:分解傳入的要刪除的事件類型types,遍歷類型,如果要刪除的事件沒有事件名,
//只有命名空間則表示刪除該命名空間下所有綁定事件
// Once for each type.namespace in types; type may be omitted
//分解types爲type.namespace爲單位元素的數組
types = (types || "").match(core_rnotwhite) || [""];
t = types.length;
//對所有的事件類型進行遍歷
while (t--) {
//rtypenamespace = /^([^.]*)(?:\.(.+)|)$/;
//如打印[click.test,click,test]
tmp = rtypenamespace.exec(types[t]) || [];
type = origType = tmp[1];
//對付命名空間存在多個的情況,如: .aaa.bbb.ccc
namespaces = (tmp[2] || "").split(".").sort();
// Unbind all events (on this namespace, if provided) for the element
//如果teype不存在,那麼移除所有的事件!也就是當前元素的events域中間的所有的數據!
if (!type) {
for (type in events) {
jQuery.event.remove(elem, type + types[t], handler, selector, true);
}
continue;//循環繼續
}
//第二步: 遍歷類型過程中,刪除匹配的事件,代理計數修正
//獲取該類型事件的special進行特殊處理
special = jQuery.event.special[type] || {};
//如果存在selector那麼就是代理對象,否則就是綁定事件到elem上面!
type = (selector ? special.delegateType : special.bindType) || type;
//獲取回調函數集合
handlers = events[type] || [];
//創建該命名空間下的一個正則表達式,例如:重新組合爲:xx.aaa.bbb.ccc
tmp = tmp[2] && new RegExp("(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)");
// Remove matching events
origCount = j = handlers.length;
while (j--) {
handleObj = handlers[j];//獲取該類型事件的所有的handleObj事件
//判斷該對象的origType,如果和handleObj一樣表示該類事件全部要移除!
//但是必須移除的對象是origType,guid,namespace,selector都相同纔可以!
if ((mappedTypes || origType === handleObj.origType) &&
(!handler || handler.guid === handleObj.guid) &&
(!tmp || tmp.test(handleObj.namespace)) &&
(!selector || selector === handleObj.selector || selector === "**" && handleObj.selector)) {
handlers.splice(j, 1);//刪除handlers[j];
if (handleObj.selector) {
//如果是selector存在表示代理對象,那麼把delegateCount遞減!
handlers.delegateCount--;
}
if (special.remove) {
//如果special有remove方法,那麼直接調用special的remove方法!
special.remove.call(elem, handleObj);
}
}
}
//第三步:如果節點上指定類型的事件處理器已經爲空,則將events上的該類型的事件處理對象移除
//例如 var js_obj = document.createElement("div"); js_obj.onclick = function(){ …}
/*上面的js_obj是一個DOM元素的引用,DOM元素它長期在網頁當中,不會消失,
而這個DOM元素的一屬性onclick,又是內部的函數引用(閉包),
而這個匿名函數又和js_obj之間有隱藏的關聯(作用域鏈)所以形成了一個,循環引用*/
//如果當前事件的回調函數集合已經爲空,
if (origCount && !handlers.length) {
//同時該speical沒有tearDown或者tearDown是false,那麼用removeEvent方法!
if (!special.teardown || special.teardown.call(elem, namespaces, elemData.handle) === false) {
jQuery.removeEvent(elem, type, elemData.handle);
}
delete events[type];//移除當前回調函數集合!
}
}
// Remove the expando if it's no longer used
//如果events對象已經是空了,那麼直接連handle也移除,因爲events不存在那麼handle已經沒有存在的意義了!
//所以移除handle同時連events域也同時移除!
if (jQuery.isEmptyObject(events)) {
delete elemData.handle;
data_priv.remove(elem, "events");
}
},
4.4 jQuery事件流程
那麼JQuery爲了更好的對事件的支持內部又做了哪些額外的優化操作?
兼容性問題處理:
瀏覽器的事件兼容性是一個令人頭疼的問題。IE的event在是在全局的window下, 而mozilla的event是事件源參數傳入到回調函數中。還有很多的事件處理方式也一樣
JQuery提供了一個 event的兼容類方案
jQuery.event.fix 對遊覽器的差異性進行包裝處理
例如:
- 事件對象的獲取兼容,IE的event在是在全局的window,標準的是event是事件源參數傳入到回調函數中
- 目標對象的獲取兼容,IE中採用srcElement,標準是target
- relatedTarget只是對於mouseout、mouseover有用。在IE中分成了to和from兩個Target變量,在mozilla中 沒有分開。爲了保證兼容,採用relatedTarget統一起來
- event的座標位置兼容
- 等等
事件的存儲優化:
jQuery並沒有將事件處理函數直接綁定到DOM元素上,而是通過.data存儲在緩存.data存儲在緩存.cahce上,這裏就是之前分析的貫穿整個體系的緩存系統了
聲明綁定的時候:
- 首先爲DOM元素分配一個唯一ID,綁定的事件存儲在.cahce[唯一ID][.cahce[唯一ID][.expand ][ 'events' ]上,而events是個鍵-值映射對象,鍵就是事件類型,對應的值就是由事件處理函數組成的數組,最後在DOM元素上綁定(addEventListener/ attachEvent)一個事件處理函數eventHandle,這個過程由 jQuery.event.add 實現。
執行綁定的時候:
- 當事件觸發時eventHandle被執行,eventHandle再去$.cache中尋找曾經綁定的事件處理函數並執行,這個過程由 jQuery.event. trigger 和 jQuery.event.handle實現。
- 事件的銷燬則由jQuery.event.remove 實現,remove對緩存$.cahce中存儲的事件數組進行銷燬,當緩存中的事件全部銷燬時,調用removeEventListener/ detachEvent銷燬綁定在DOM元素上的事件處理函數eventHandle。
事件處理器:
jQuery.event.handlers
針對事件委託和原生事件(例如"click")綁定 區分對待
事件委託從隊列頭部推入,而普通事件綁定從尾部推入,通過記錄delegateCount來劃分,委託(delegate)綁定和普通綁定。
其餘一些兼容事件的Hooks
fixHooks,keyHooks,mouseHooks
總的來說對於JQuery的事件綁定
在綁定的時候做了包裝處理
在執行的時候有過濾器處理。
下章再看具體流程分解。