1. 程式人生 > >事件冒泡初認識

事件冒泡初認識

說是初認識,其實也不算了,剛學習JS時就已經聽過事件的冒泡和捕獲的大名,但真是不知所云,也是不求甚解,迷惑了很久,今天終決定好好來了解一下這個冒泡。

什麼是冒泡

當事件發生後,這個事件就要開始傳播(從裡到外或者從外向裡)。為什麼要傳播呢?因為事件源本身(可能)並沒有處理事件的能力,即處理事件的函式(方法)並未繫結在該事件源上。例如我們點選一個按鈕時,就會產生一個click事件,但這個按鈕本身可能不能處理這個事件,事件必須從這個按鈕傳播出去,從而到達能夠處理這個事件的程式碼中(例如我們給按鈕的onclick屬性賦一個函式的名字,就是讓這個函式去處理該按鈕的click事件),或者按鈕的父級繫結有事件函式,當該點選事件發生在按鈕上,按鈕本身並無處理事件函式,則傳播到父級去處理。

 按我自己的理解,冒泡就真的是魚兒吐泡泡一樣,從水底冒一個泡咕嚕咕嚕漂到了水面,這個順序是自下往上的;而捕獲呢,就像捕魚一樣從水面上撒網沉到水底,這個順序是自上往下的。這樣類比到DOM樹中應該就能夠記得很形象了。也就是說,事件冒泡 :當一個元素接收到事件的時候,會把它接收到的事件傳給自己的父級,一直到window。

但有一點需要注意的,這裡傳遞的僅僅是事件,並不傳遞所繫結的事件函式。所以如果父級沒有繫結事件函式,就算傳遞了事件,也不會有什麼表現,但事件確實是傳遞了。我們用程式碼來具體的表示,

 HTML結構:

<div id="div1" style="width: 50px;height: 50px;background: red;">
    <div id="div2" style="width: 30px;height: 30px;background: blue;"></div>
</div>

JS部分:

var div1 = document.getElementById("div1");
var div2 = document.getElementById("div2");
div2.onclick = function(){
    alert("div2");
}
div1.onclick = function(){
    alert("div1");
}

這段小程式碼很簡單,定義了一個父子關係的div元素,分別增加了點選事件。當我們點選子元素div2時,先會彈出它自身的事件“div2”,然後又彈出了“div1”。這就說明了點選子元素的時候不僅僅觸發了子元素自己的事件,它的父元素事件也被觸發 了。這樣的現象就叫做冒泡。

再舉個例子,還是剛剛的程式碼,我們來做一些小修改,刪除子元素的點選事件!

可以看一下最終的效果圖,當點選子元素div2時,會彈出事件“div1”,這再次證明了觸發了冒泡模式,當點選了沒有繫結事件的子元素,父元素的點選事件被觸發,執行了自己繫結的函式。此外,這種事件的觸發,和CSS樣式是沒有關係的。可以改變父子元素的position位置,分離兩個元素的位置,這時仍會觸發冒泡。

當然,如果只刪除了父元素div1的點選事件,那點選子元素時只會觸發子元素的事件,而傳遞給父元素的只是事件,但因為父元素自身沒有繫結任何事件,就算傳遞給它事件,父元素仍然無法觸發事件。

冒泡的優點

至此,我們算是初認識了事件冒泡,會不會感覺這沒什麼用呀,其實冒泡真有一大優點,就是產生了事件委託。所謂事件委託:

事件委託: 由於事件的冒泡,我們點選子元素的時候,會把事件一層層的傳遞給父級元素。相反的,我們操作元素的時候,直接把事件繫結在父級元素上,而不是分別給子元素繫結事件。通過判斷子元素,從而達到同樣的效果

先上例子,假設情形:有5個列表,我們點選其中一個就會改變其背景顏色為紅色。

 HTML結構很簡單:

<ul id="ul">
    <li>這就是個測試</li>
    <li>這就是個測試</li>
    <li>這就是個測試</li>
    <li>這就是個測試</li>
    <li>這就是個測試</li>
</ul>

JS部分:

var ul = document.getElementById("ul");
var li = ul.getElementsByTagName("li");
for(var i=0; i<li.length; i++){
    li[i].onclick = function(){
        this.style.background = "red";
    }
}

通過迴圈給每一個 li 加事件,感覺應該也沒什麼問題,但其實這是非常耗費效能的。就算不考慮效能吧,那就假設一個很極端的情況,如果我想在第6個位置新增一個div元素,第7個位置新增一個p元素,同樣的內容要實現同樣的效果,那是不是就需要另外給這個div和p分別新增一個點選事假,這就很麻煩了。

如果我們可以給父元素ul新增事件,作用於全部子元素,那不是一勞永逸嘛,而這個就是通過冒泡模式進行的事件委託實現的。但是這就會有個問題,我選中的是ul,那麼點選其中的任一 li 時,豈不是全部的 ul 列表都變紅了嘛,這與初始需求相違背了。這就需要新概念的登場了,能夠準確獲取你當前滑鼠所指的 li 的事件源。

事件源:不管事件繫結在那個元素中,都指的是實際觸發事件的那個的目標

這也存在IE的相容性問題:

  • IE:window.event.srcElement
  • 標準的W3C 方式:event.target

 那麼做一下相容性處理,JS部分變為:

var ul = document.getElementById("ul");
var li = ul.getElementsByTagName("li");

ul.onclick = function(ev){
    var event = ev || window.event;
    var target = event.target ||event.srcElement;
    target.style.background = "red";
}

這樣就很好的提高了效能,而且在增加其他子元素時候,同樣可以滿足需求。

冒泡的缺點

但是喲,冒泡模式也存在一些困擾,譬如假設一個情形:

需求是,點選div1時候,能夠讓div2顯示,再點選其他地方時會讓div2隱藏。那JS可以這樣寫: 

var div1 = document.getElementById("div1");
var div2 = document.getElementById("div2");

div1.onclick = function(){
    div2.style.display = "block";
}
document.onclick = function(){
    div2.style.display = "none";
}

去瀏覽器驗證最終效果,很神奇的發現點選了紅色部分,藍色方塊消失了!明明是需要讓它顯示的嘛,看一下我們寫的程式碼,邏輯很簡單直白,完全無懈可擊呀。

這就是冒泡惹的禍,由於事件冒泡,點選div1時候,事件向上傳遞,一直傳到了document,而此時的document也綁定了自己的事件,那就正好觸發了,也就是讓藍色方塊隱藏了。所以,也不能說點選紅色方塊沒有讓藍色方塊顯示,而是這個過程太短暫 了,還沒顯示呢就已經觸發了最終的document的事件。可以在div1事件中新增一個彈框用來測試是否進行了這一步驟。

因此,這種情況下我們就不希望存在冒泡了,那就需要用到取消事件的冒泡的兩種方式:

  • 標準的W3C 方式:e.stopPropagation();這裡的stopPropagation是標準的事件物件的一個方法,呼叫即可
  • 非標準的IE方式:ev.cancelBubble=true;  這裡的cancelBubble是 IE事件物件的屬性,設為true就可以了

因為這存在IE和其他瀏覽器的差異,所以會做一個判斷,封裝成一個函式:

function stopBubble(e) {
   if ( e && e.stopPropagation )
      e.stopPropagation();
  else
    window.event.cancelBubble = true;
}

首先,判斷這個瀏覽器是否支援stopPropagation(),如果支援說明是非IE,可以使用標準的W3C方式,不然就是用IE提供的辦法。有了這樣一個阻止冒泡的函式,那就在div1事件中呼叫這個函式,讓我們點選div1時不會再往上冒泡傳遞事件,也就不會觸發到document的事件了,這樣當初的需求完美實現了。