事件冒泡 和 事件捕獲

weixin_33860722發表於2016-10-17

背景知識

什麼是事件?
直觀的說就是網頁上發生的事情,大部分是指使用者的滑鼠動作和鍵盤動作,如點選、移動滑鼠、按下某個鍵。為什麼說大部分呢,因為事件不單單隻有這兩部分,還有其他的例如document的load和unloaded。只不過我們更加關注的是使用者的操作。

事件模型:規範事件的定義的一種標準

事件的三種模型:

  • 原始事件模型(DOM 0級事件模型)
  • IE事件模型
  • DOM2事件模型
DOM2事件模型:

此模型是W3C制定的標準模型,既然是標準,那大家都得按這個來,我們現在使用的現代瀏覽器(指IE6~8除外的瀏覽器)都已經遵循這個規範。W3C制定的事件模型中,“DOM2級事件”中規定的事件流同時支援了事件捕獲階段和事件冒泡階段,而作為開發者,我們可以選擇事件處理函式在哪一個階段被呼叫。
一次事件的發生包含三個過程:

(1)capturing phase:事件捕獲階段。
事件被從document一直向下傳播到目標元素,在這過程中依次檢查經過的節點是否註冊了該事件的監聽函式,若有則執行。

(2)target phase:事件處理階段。
事件到達目標元素,執行目標元素的事件處理函式.

(3)bubbling phase:事件冒泡階段。
事件從目標元素上升一直到達document,同樣依次檢查經過的節點是否註冊了該事件的監聽函式,有則執行。

所有的事件型別都會經歷captruing phase(事件捕獲),但是隻有部分事件會經歷bubbling phase(事件冒泡)階段,例如submit事件就不會被冒泡。

1. 是什麼?

<div id="outer">
    <p id="inner">Click me!</p>
</div>

上面的程式碼當中一個div元素當中有一個p子元素,如果兩個元素都有一個click的處理函式,那麼我們怎麼才能知道哪一個函式會首先被觸發呢?

為了解決這個問題微軟和網景提出了兩種幾乎完全相反的概念。

**事件流 : ** 指的是頁面中接收事件的順序.

事件冒泡

微軟提出了名為事件冒泡(event bubbling)的事件流。
事件冒泡可以形象地比喻為把一顆石頭投入水中,泡泡會一直從水底冒出水面。

當一個DOM元素上的事件被觸發的時候(如:按鈕點選事件),這個元素的所有父元素 中,如果也繫結有該相同事件,則也會被觸發, 觸發的順序就是先從 : 當前元素的事件 ==> 臨近父元素 ==> 父元素......,這一過程被稱為事件冒泡

因此上面的例子在事件冒泡的概念下發生click事件的順序應該是:
p -> div -> body -> html -> document
IE,火狐和chrome瀏覽器都是事件冒泡.

事件捕獲

網景提出另一種事件流名為事件捕獲(event capturing)。
當一個DOM元素上的事件被觸發的時候(如:按鈕點選事件),這個元素的所有父元素 中,如果也繫結有該相同事件,則也會被觸發, 觸發的順序就是先從 : ....... 父元素 ==> 臨近父元素 ==> 當前元素的事件,這一過程被稱為事件捕獲
與事件冒泡相反,事件會從最外層開始發生,直到最具體的元素。
上面的例子在事件捕獲的概念下發生click事件的順序應該是:
document -> html -> body -> div -> p

圖解:


2039222-f854f588a43413ed.png
事件冒泡和捕獲具體如圖

2. 解決了什麼問題?

這兩個概念都是為了解決頁面中事件流(事件發生順序)的問題。

http://www.imooc.com/article/9833

3. 執行原理

<ul>
   <li>
       <p>
          <a>  </div>
      </p>
   </li>
</ul>
2039222-940b328b1f2bd63f.png
事件冒泡和事件捕獲原型圖

事件捕獲階段:事件從最上一級標籤開始往下查詢,直到捕獲到事件目標(target)。
事件冒泡階段:事件從事件目標(target)開始,往上冒泡直到頁面的最上一級標籤。

事件捕獲當你使用事件捕獲時,父級元素先觸發,子級元素後觸發,即div先觸發,p後觸發。
事件冒泡當你使用事件冒泡時,子級元素先觸發,父級元素後觸發,即p先觸發,div後觸發。

4. 如何使用?

說到事件的執行順序,那麼我們需要知道一個給元素新增事件的方法, 即給某元素動態繫結事件
W3C為我們提供了addEventListener()函式用來為指定的dom元素動態繫結事件。

語法:
element.addEventListener( event ,  function ,  useCapture )

2039222-381e352487a15749.png
引數分析

提示: 使用 removeEventListener()方法來移除 addEventListener() 方法新增的事件控制程式碼。

function sayHello() { 
console.log("hello");
}
var myDiv = document.getElementById("myDiv");
myDiv.addEventListener("click", sayHello);

這樣我們點選id為myDiv的元素時,控制檯就會輸出"Hello"。
事件冒泡

有如下html程式碼:


2039222-6051a180bbdc7527.png
html程式碼
2039222-c0a70c93b60cd382.png
執行結果

下面設定了四個函式用來進行事件繫結:


2039222-a5be3575bf670261.png
要繫結的函式

使用下面的程式碼,我們可以獲取四個元素對應DOM


2039222-e7b0d28e075774e6.png
四個DOM元素

現在,我試著同時分別為grandpa和grandson繫結sleep和doingHomework事件:

2039222-fe31451e59598e7b.png
繫結事件

這時我們點選最外層的grandpa時,當然會觸發sleep函式,然而當我們點選grandson時,控制檯的輸出如下:

2039222-c92819966f1013a7.png
控制檯檢視結果

原因:這是因為grandson在grandpa之上,當點選grandson時,同時也在grandpa上進行了點選操作,所以在執行了doingHomework後,還會觸發grandpa的sleep函式。
這種當滿足條件後從子元素到父元素依次觸發其上事件的處理方式叫做事件冒泡

我們也為father和child分別繫結watchTV和playingCard函式

2039222-1eab0b239c64d33e.png
繫結事件
2039222-1bf92bdecac9b0e9.png
控制檯檢視結果
事件冒泡()
grandpa.addEventListener("click", sleep);            
grandson.addEventListener("click", doingHomework);            
father.addEventListener("click", watchTV);            
child.addEventListener("click", playingCard);
2039222-546f70fa395362de.png
四個元素都繫結相同事件,點選grandson時的結果
2039222-2256bece43ec4fa5.png
點選child元素的 結果

事件捕獲

事件捕獲與事件冒泡完全相反,先觸發祖先元素的事件,然後再逐級觸發子元素的事件。預設情況下,繫結事件時,採用事件冒泡原則,如果想要進行事件捕獲的話,需要設定一個引數 。

可以為addEventListener函式新增第三個引數useCapture,引數值是布林值,預設是false。當useCapture為false時,事件處理採取事件冒泡的原則,當userCapture為true時,則採取事件捕獲的原則

2039222-7c85375278e08bab.png
繫結捕獲事件

這時,當點選grandson時,就會先執行祖先元素的事件,再執行後代元素的事件了,控制檯的輸出如下圖所示:

2039222-1679e1761e8e83bf.png
捕獲事件的執行結果

雖然預設情況下,useCapture的值是false,但我推薦我們在繫結函式時把它明顯的寫出來以避免瀏覽器相容性的問題。

事件冒泡與事件捕獲要是同時進行怎麼辦

有思想的同學肯定會思考這樣一個問題,在上述繫結事件的程式碼中,第三個引數不是全部設定的true,就是全部設定成false,那如果既有true,又有false,有的元素設定成按事件冒泡處理,有的元素設定成按事件捕獲處理,那怎麼辦呢?
直接告訴大家答案,我們的瀏覽器更“喜愛”事件捕獲:

它會先把useCapture為false的元素繫結事件放到一邊,按照事件捕獲正常的順序執行useCapture為true的元素繫結事件,最後在按照事件冒泡順序執行useCapture為false。
現在我們作如下更改

2039222-4773ca744641c624.png
既有事件冒泡又有事件捕獲

按照上述原則,當點選grandson時,先執行useCapture為true的元素的繫結事件,又按照事件捕獲原則,先執行grandpa的事件,再執行child的事件。之後,再按照事件捕獲順序執行useCapture為false的事件,輸出結果如下:
2039222-bb449722178ee380.png
細說addEventListener與事件捕獲、事件冒泡_

阻止事件冒泡和捕獲

我們可以利用時間物件event的stopPropagation()方法阻止事件的進一步傳播。
我們修改一下doingHomework函式:

2039222-b2a2439257c5fd74.png
為grandson的事件設定阻止事件物件
2039222-0cd0e85876cbfc31.png
設定阻止事件後,點選grandson的結果

發現事件執行到doingHomework就被阻斷了,其後不會在事件傳播到父元素。
值得注意的是,event.stopPropagation()函式並不會阻止其下函式內容的執行。

*如果你使用的是jquery的事件繫結,也可以直接在函式中使用return false來阻止事件的傳播(當然event.stopPropagation同樣有效),與event.stopPropagation不同的是,return false會阻止同函式下面的程式碼執行 *

傳統繫結事件方式在一個支援W3C DOM的瀏覽器中,像這樣一般的繫結事件方式,是採用的事件冒泡方式。
ele.onclick = doSomething2

IE瀏覽器
如上面所說,IE只支援事件冒泡,不支援事件捕獲,它也不支援addEventListener函式,不會用第三個引數來表示是冒泡還是捕獲,它提供了另一個函式attachEvent。
ele.attachEvent("onclick", doSomething2);

不是所有的事件都能冒泡,例如:blur、focus、load、unload。

事件冒泡的好處

相關文章