事件冒泡和事件代理

有關(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ì)事件支持的不同.

  1. 對(duì)于IE,jQuery直接使用IE中已經(jīng)支持冒泡的事件focusin,對(duì)于用戶想要注冊(cè)綁定的focus事件,直接將其綁定到focusin事件上實(shí)現(xiàn)事件冒泡,

  2. 對(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):

  1. 鼠標(biāo)點(diǎn)擊類型為submit的button,觸發(fā)click事件

  2. 如果button有對(duì)應(yīng)的click事件監(jiān)聽,則執(zhí)行監(jiān)聽函數(shù),否則click事件冒泡到上級(jí)元素

  3. 2 中click事件監(jiān)聽函數(shù)執(zhí)行完畢返回,若返回值為非false,click事件冒泡到上級(jí)元素,否則click事件冒泡終止,表單提交動(dòng)作不執(zhí)行(默認(rèn)行為不執(zhí)行)

  4. 事件冒泡到上級(jí)元素,重復(fù)過(guò)程 2, 3,直至遍歷到window對(duì)象或者中途監(jiān)聽函數(shù)返回false中止。

  5. 事件冒泡到頂層元素window,如返回值不為false,則執(zhí)行默認(rèn)行為,對(duì)應(yīng)為觸發(fā)form表單的submit事件

  6. 由于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的方案如下:

  1. 文檔加載時(shí),如果存在form表單的事件代理,為代理對(duì)象delegateObj綁定click,keypress事件監(jiān)聽fn

  2. button觸發(fā)click或者keypress事件后,事件冒泡到delegateObj,觸發(fā)監(jiān)聽函數(shù)fn,fn中找到事件源對(duì)應(yīng)的form元素,為form元素綁定submit事件監(jiān)聽函數(shù)fn_submit,

  3. click事件冒泡到頂層元素window對(duì)象,執(zhí)行submit類型click事件的默認(rèn)行為

  4. 表單提交事件被觸發(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綜合判斷

  1. 鼠標(biāo)離開layer,target必須為layer

  2. 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事件。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • (續(xù)jQuery基礎(chǔ)(1)) 第5章 DOM節(jié)點(diǎn)的復(fù)制與替換 (1)DOM拷貝clone() 克隆節(jié)點(diǎn)是DOM的常...
    凜0_0閱讀 1,530評(píng)論 0 8
  • 1.JQuery 基礎(chǔ) 改變web開發(fā)人員創(chuàng)造搞交互性界面的方式。設(shè)計(jì)者無(wú)需花費(fèi)時(shí)間糾纏JS復(fù)雜的高級(jí)特性。 1....
    LaBaby_閱讀 1,515評(píng)論 0 2
  • 1.JQuery 基礎(chǔ) 改變web開發(fā)人員創(chuàng)造搞交互性界面的方式。設(shè)計(jì)者無(wú)需花費(fèi)時(shí)間糾纏JS復(fù)雜的高級(jí)特性。 1....
    LaBaby_閱讀 1,277評(píng)論 0 1
  • 好像沒(méi)3天就看完了吧,這是比較快的一次看書,收獲也不少,總結(jié)一下趕快輸出吧,以前看完書只有迷糊的東西,寫不了多少的...
    一縷桂花閱讀 223評(píng)論 0 0

友情鏈接更多精彩內(nèi)容