有關(guān)jQuery 事件模塊結(jié)構(gòu)部分的分析可以參考這篇文章,作者分析的很不錯(cuò),贊一個(gè)。
進(jìn)題之前,有幾個(gè)名詞 EventTarget,EventListener,Event講一下,文章中會(huì)用到
var t = document.getElementsByTagName('div')[0],
fn = function( e ){
console.log( e.type) ;
};
t.addEventListener('click' , fn , false);
解釋上面代碼就是:EventTarget為t的元素div注冊(cè)了一個(gè)EventType為click的事件監(jiān)聽EventListener fn,事件監(jiān)聽函數(shù)fn有一個(gè)Event類型的參數(shù)e
下面的代碼有寫過(guò)沒(méi)?
jquery事件綁定相關(guān)的核心API,代碼中蘊(yùn)含了一種設(shè)計(jì)邏輯:對(duì)于from表單的submit事件,委托給body元素,由fn監(jiān)聽函數(shù)去處理,這種設(shè)計(jì)模式稱為事件代理。
不清楚下面代碼邏輯的,可以參考這里 delegate,live,on,三者之間的區(qū)別可以看這篇文章
$("body").delegate( "form", "submit" , fn );
$("body").live( "submit" , fn );
$("body").on( "submit" , "form", fn );
什么是事件代理?
舉個(gè)例子:假設(shè)元素E的上級(jí)元素F監(jiān)聽了click事件,處理函數(shù)為fn,元素E并沒(méi)有監(jiān)聽click事件,鼠標(biāo)點(diǎn)擊了元素E,由于事件冒泡原理,fn會(huì)因?yàn)镋的click操作而被執(zhí)行,這一過(guò)程描述的就是F代理了E的click事件
如下圖:

要想這一過(guò)程有意義,EventListenerfn需要具備一個(gè)能力:在fn中能夠訪問(wèn)到觸發(fā)事件的元素E,為什么?因?yàn)榇蠖嗲闆r下需要改變的是觸發(fā)事件的元素的屬性,而不是事件代理本身。
要訪問(wèn)EventTarget E,可以這樣做:
e = e || window.event ;
target = e.target || e.srcElement
事件代理帶來(lái)了什么好處
看下圖

E1 , E2…En是F的子元素,假設(shè)子元素觸發(fā)的事件都是相同的,按照逐一綁定事件的做發(fā),代碼就是下面的樣子,不夠優(yōu)雅不夠節(jié)約
E1.bind( 'eventType' , eventListioner );
E2.bind( 'eventType' , eventListioner );
......
En.bind( 'eventType' , eventListioner );
有了事件代理,注冊(cè)事件的代碼就變成這樣子了
F.delegate( 'eventType' , eventListioner );
function eventListioner ( e ){
if(e.target.tagName == E){
....
}
}
還沒(méi)完,前面的一丟丟都是在假設(shè)事件是可以冒泡的前提下,是否存在不冒泡的事件呢?如果存在,那代理模型應(yīng)用在DOM事件模型上無(wú)疑存在缺陷,不好意思,input元素的focus事件默認(rèn)是不冒泡的,為什么呢?ppk前輩也沒(méi)找到根源,但是與對(duì)其他元素影響較小不無(wú)關(guān)系。其他不冒泡的事件可以參考這里
模擬事件冒泡
既然存在不冒泡的事件,為了確保瀏覽器之間的兼容,就需要為不冒泡的事件添加冒泡特性。
冒泡的本質(zhì)是什么?一句話描述冒泡的過(guò)程就是:事件源(EventTarget)A觸發(fā)一事件E,瀏覽器會(huì)檢查EventTarget A是否有注冊(cè)對(duì)應(yīng)的事件處理函數(shù),如果有對(duì)應(yīng)的處理函數(shù),則調(diào)用,否則,事件傳遞到父節(jié)點(diǎn)AF上,然后重復(fù)前一過(guò)程直至事件傳遞到Window對(duì)象終止。
其本質(zhì)就是事件在DOM元素上自下而上的傳遞事件,執(zhí)行事件監(jiān)聽的過(guò)程,清楚了原理,模擬事件冒泡的算法也就有了(simulate_bubble_algo):
算法以事件觸發(fā)者為起點(diǎn),以window對(duì)象為終點(diǎn),在DOM層級(jí)樹上做遍歷,判斷DOM元素是否有注冊(cè)對(duì)應(yīng)EventType的事件監(jiān)聽。
jQuery中讓input元素的focus事件冒泡做法, jQuery 利用了瀏覽器之間對(duì)事件支持的不同.
對(duì)于IE,jQuery直接使用IE中已經(jīng)支持冒泡的事件focusin,對(duì)于用戶想要注冊(cè)綁定的focus事件,直接將其綁定到focusin事件上實(shí)現(xiàn)事件冒泡,
對(duì)于非IE瀏覽器,jQuery在focus事件捕獲階段為document綁定監(jiān)聽函數(shù),該函數(shù)實(shí)現(xiàn)simulate_bubble_algo算法,對(duì)DOM樹的遍歷,逐一觸發(fā)滿足條件的元素的事件監(jiān)聽函數(shù)。
還沒(méi)完,IE<9中form元素的submit事件也是不冒泡的,與focus事件相比,為IE添加冒泡的submit事件似乎更為緊迫。
為了描述jQuery中的實(shí)現(xiàn)方案,先要解釋一些現(xiàn)象:
- 理解事件的默認(rèn)行為
HTML中type為submit的兩類元素input和button,與其他類型非submit的元素相比較,他們多的是有一個(gè)submit form表單的默認(rèn)行為
<input id="asubmitInput" type="submit" />
<button id="asubmitButton" type="submit" />
- 理解 event.preventDefault()的作用(IE中對(duì)應(yīng)是event.returnValue), Event對(duì)象的該方法/屬性用于阻止瀏覽器執(zhí)行事件的默認(rèn)行為。
OK,jQuery如何實(shí)現(xiàn)submit事件的冒泡呢?
想象一下button提交表單的過(guò)程(submit_event_flow):
鼠標(biāo)點(diǎn)擊類型為submit的button,觸發(fā)click事件
如果button有對(duì)應(yīng)的click事件監(jiān)聽,則執(zhí)行監(jiān)聽函數(shù),否則click事件冒泡到上級(jí)元素
2 中click事件監(jiān)聽函數(shù)執(zhí)行完畢返回,若返回值為非false,click事件冒泡到上級(jí)元素,否則click事件冒泡終止,表單提交動(dòng)作不執(zhí)行(默認(rèn)行為不執(zhí)行)
事件冒泡到上級(jí)元素,重復(fù)過(guò)程 2, 3,直至遍歷到window對(duì)象或者中途監(jiān)聽函數(shù)返回false中止。
事件冒泡到頂層元素window,如返回值不為false,則執(zhí)行默認(rèn)行為,對(duì)應(yīng)為觸發(fā)form表單的submit事件
由于submit事件在IE中不冒泡,所以至此form的submit操作結(jié)束。
驗(yàn)證這一過(guò)程可以使用下面的腳本
$(function(){
var elem = document.getElementsByTagName("input")[0];
var aform = document.getElementsByTagName("form")[0];
if(elem.addEventListener){
aform.addEventListener("submit" , function(){
console.log("form submit fired");
});
window.addEventListener("click" , function(){
console.log("window click fn fired");
});
document.addEventListener("click" , function(){
console.log("document click fn fired");
});
document.documentElement.addEventListener("click" , function(){
console.log("html click fn fired");
});
document.body.addEventListener("click" , function(){
console.log("body click fn fired");
});
elem.addEventListener("click" , function(e){
console.log("input click fn fired");
});
}
});
<form action='#' >
<input type="submit"/>
</form>
如何修復(fù)IE中的這一問(wèn)題,使得IE的submit事件冒泡呢?jQuery的方案如下:
文檔加載時(shí),如果存在form表單的事件代理,為代理對(duì)象delegateObj綁定click,keypress事件監(jiān)聽fn
button觸發(fā)click或者keypress事件后,事件冒泡到delegateObj,觸發(fā)監(jiān)聽函數(shù)fn,fn中找到事件源對(duì)應(yīng)的form元素,為form元素綁定submit事件監(jiān)聽函數(shù)fn_submit,
click事件冒泡到頂層元素window對(duì)象,執(zhí)行submit類型click事件的默認(rèn)行為
表單提交事件被觸發(fā),執(zhí)行fn_submit監(jiān)聽函數(shù),該監(jiān)聽函數(shù)修改Event對(duì)象屬性,確保事件分發(fā)過(guò)程中執(zhí)行一次simulate_bubble_algo算法。
事件冒泡的一個(gè)意外
因?yàn)槭录芭輽C(jī)制的原因,mouseover和mouseout事件中對(duì)鼠標(biāo)位置的正確判斷變得復(fù)雜,為此W3C標(biāo)準(zhǔn)中新加入了默認(rèn)不冒泡的鼠標(biāo)事件mouseenter和mouseleave,但是瀏覽器廠商似乎并不買賬,遲遲并未實(shí)現(xiàn)新的鼠標(biāo)事件,為此PPK在其Blog上大吐苦水,希望瀏覽器廠商把這事當(dāng)回事,畢竟用戶需要不冒泡的mouseover和mouseout事件。
講了這麼多,描述一下mouseover 和 mouseout實(shí)際開發(fā)中遇到的問(wèn)題。下面文字絕大部分取自PPK的博客該篇文章
假設(shè)要監(jiān)聽ev4元素的鼠標(biāo)的mouseover事件,圖1鼠標(biāo)從ev3移動(dòng)到ev4,圖二鼠標(biāo)從span移動(dòng)到ev4,盡管事件最終都是在ev4元素上觸發(fā),但知道鼠標(biāo)從哪里來(lái)很有必要,為此W3C定義了一個(gè)屬性relatedTarget,對(duì)于mouseover事件,這個(gè)屬性記錄鼠標(biāo)從哪里來(lái),對(duì)于mouseout記錄鼠標(biāo)去了哪里,但低版本IE并不支持該屬性,好在IE有對(duì)應(yīng)的屬性fromElement/toElement.


看下圖,程序在layer上注冊(cè)了mouseout事件,監(jiān)聽鼠標(biāo)是否離開layer,但有一情況,例如鼠標(biāo)從Link移動(dòng)到layer的過(guò)程中,由于事件冒泡,也會(huì)導(dǎo)致綁定在layer上的事件監(jiān)聽器doSomething被執(zhí)行,初衷是監(jiān)聽鼠標(biāo)離開layer,但實(shí)際上鼠標(biāo)并未離開layer就執(zhí)行了事件監(jiān)聽。

如何解決這一問(wèn)題?PPK給出了一個(gè)解決方案,思路就是要根據(jù)event的屬性target和relatedTarget綜合判斷
鼠標(biāo)離開layer,target必須為layer
1成立,判斷relatedTarget對(duì)象和target之間的關(guān)系,target不能是relatedTarget的祖先元素
實(shí)現(xiàn)的代碼如下:
function doSomething(e) {
if (!e) var e = window.event;
var tg = (window.event) ? e.srcElement : e.target;
if (tg.nodeName != 'DIV') {
return;
}
var reltg = (e.relatedTarget) ? e.relatedTarget : e.toElement;
while (reltg != tg && reltg.nodeName != 'BODY'){
reltg= reltg.parentNode if (reltg== tg)
}
return;
}
世界上總是不缺少有心人,盡管除了IE外其他瀏覽器廠商并沒(méi)有實(shí)現(xiàn)mouseenter和mouseleave事件,但是jQuery卻在其代碼中為我們模擬出了mouseenter和mouseleave事件。