JS中的事件順序(事件捕獲與冒泡)

subwaydown發表於2018-03-16

一. 問題

如果一個元素和它的祖先元素註冊了同一型別的事件函式(例如點選等), 那麼當事件發生時事件函式呼叫的順序是什麼呢?

比如, 考慮如下巢狀的元素:

-----------------------------------
| outer                           |
|   -------------------------     |
|   |inner                  |     |
|   -------------------------     |
|                                 |
-----------------------------------
複製程式碼

兩個元素都有onclick的處理函式. 如果使用者點選了inner, innerouter上的事件處理函式都會被呼叫. 但誰先誰後呢?

二. 兩個模型

在剛剛過去的那些糟糕年代, Netscape和M$對此有不同的看法.

Netscape認為outer上的處理函式應該先被執行. 這被稱作event capturing.

M$則認為inner上的處理函式具有執行優先權. 這被叫做event bubbling.

事件捕獲(event capturing)

               | |
---------------| |-----------------
| outer        | |                |
|   -----------| |-----------     |
|   |inner     \ /          |     |
|   -------------------------     |
|        Event CAPTURING          |
-----------------------------------
複製程式碼

outer上的事件處理器先觸發, 然後是inner上的.

事件冒泡(event bubbling)

               / \
---------------| |-----------------
| outer        | |                |
|   -----------| |-----------     |
|   |inner     | |          |     |
|   -------------------------     |
|        Event BUBBLING           |
-----------------------------------
複製程式碼

與事件捕獲相反, 當使用事件冒泡時, inner上的事件處理器先被觸發, 其後是outer上面的.

三. W3C 模型

W3C標準則取其折中方案. W3C事件模型中發生的任何事件, 先(從其祖先元素document)開始一路向下捕獲, 直到達到目標元素, 其後再次從目標元素開始冒泡.

          1. 先從上往下捕獲
                  |
                 | |  / \
-----------------| |--| |-----------------
| outer          | |  | |                |
|   -------------| |--| |-----------     |
|   |   inner    \ /  | |          |     |
|   |                  |           |     |
|   |   2. 到達目標元素後從下往上冒泡|     |
|   --------------------------------     |
|        W3C event model                 |
------------------------------------------
複製程式碼

而你作為開發者, 可以決定事件處理器是註冊在捕獲或者是冒泡階段. 如果addEventListener的最後一個引數是true, 那麼處理函式將在捕獲階段被觸發; 否則(false), 會在冒泡階段被觸發.

例如如下的程式碼:

var selector = document.querySelector.bind(document);
selector('div.outer').addEventListener('click', (e) => {
    selector('p:first-of-type').textContent += 'outer clicked! '
}, true)
selector('div.inner').addEventListener('click', (e) => {
    selector('p:first-of-type').textContent += 'inner clicked! '
}, false)
document.addEventListener('click', (e) => {
    selector('p:first-of-type').textContent += 'document clicked! '
}, true)
複製程式碼

當點選inner元素時, 如下事情發生了:

  1. 點選事件開始於捕獲階段. 在此階段, 瀏覽器會在inner的所有祖先元素上查詢點選事件處理函式(從document開始).

  2. 結果找到了2個, 分別在documentouter上面, 而且這兩個事件處理函式的useCapture選項為true, 說明它們是被註冊在捕獲階段的. 於是, documentouter的點選處理函式被執行了.

  3. 繼續向下尋找, 直到達到inner元素本身. 捕獲階段就此結束. 此時進入冒泡階段, inner上的事件處理器得到執行.

  4. 事件命中目標元素後開始向上冒泡, 一路查詢是否有註冊了冒泡階段的祖先元素上的事件處理器. 由於沒有找到, 因此什麼也沒發生.

最後的結果是:

// log
document clicked! outer clicked! inner clicked!
複製程式碼

如果我們把祖先元素的事件處理器註冊在冒泡階段的話(addEventListeneruseCapture選項為false):

var selector = document.querySelector.bind(document);
selector('div.outer').addEventListener('click', (e) => {
    selector('p:first-of-type').textContent += 'outer clicked! '
    console.log(e);
}, false)
selector('div.inner').addEventListener('click', (e) => {
    selector('p:first-of-type').textContent += 'inner clicked! '
    console.log(e);
}, false)
document.addEventListener('click', (e) => {
    selector('p:first-of-type').textContent += 'document clicked! '
}, false)
複製程式碼

結果則是:

// log
inner clicked! outer clicked! document clicked!
複製程式碼

傳統模型 element.onclick = function(){}將被註冊在冒泡階段.

四. 事件冒泡的應用

例如: 當點選時的預設函式.

如果在document上註冊一個點選函式:

document.addEventlistener('click', (e) => {}, false)

那麼任何元素上的點選事件最後都會冒泡到這個事件處理器上並觸發函式 - 除非前面的事件處理函式阻止了冒泡(e.stopPropagation(), 在這種情況下事件不會繼續向上冒泡)

注意: e.stopPropagation()只能阻止事件在冒泡階段的向上傳播.如果被點選元素的祖先元素有註冊在捕獲階段的事件處理器:

ancestorElem.addEventListner('click', (e) => {
// do something...
}, true)
複製程式碼

那麼該祖先元素上的事件處理器照樣會在捕獲階段被觸發.

因此, 你可以在document上設定這麼一個處理函式, 當頁面上的任何元素被點選時, 這個處理函式就被會觸發. 一個實用的例子就是下拉選單: 當點選文件上除下拉選單本身時任意一處時, 下拉選單會被隱藏.

在冒泡或者捕獲階段, e.currentTarget指向當前事件處理函式所附著的元素. 你也可以用事件處理函式內的this取而代之.

事件委託:

利用事件冒泡的特性,將裡層的事件委託給外層事件,根據event物件的屬性進行事件委託,改善效能。 使用事件委託能夠避免對特定的每個節點新增事件監聽器;事件監聽器是被新增到它們的父元素上,事件監聽器會分析從子元素冒泡上來的事件,找到是哪個子元素的事件。

舉個例子:滑鼠放到li上對應的li背景變灰。

<ul>
    <li>item1</li>
    <li>item2</li>
    <li>item3</li>
    <li>item4</li>
    <li>item5</li>
    <li>item6</li>
</ul>
複製程式碼

利用事件冒泡實現:

$("ul").on("mouseover",function(e){
    $(e.target).css("background-color","#ddd").siblings().css("background-color","white");
})
複製程式碼

當然也可以直接給所有li都綁上事件,例如:        

$("li").on("mouseover",function(){
    $(this).css("background-color","#ddd").siblings().css("background-color","white");
})
複製程式碼

從程式碼簡潔程度上,兩者是相若的。但是,前者少了一個遍歷所有li節點的操作,所以在效能上肯定是更優的。

還有就是,如果我們在繫結事件完成後,頁面又動態的載入了一些元素:

$("<li>item7</li>").appendTo("ul");
複製程式碼

這時候,第二種方案,由於繫結事件的時候item7還不存在,所以還要給它再繫結一次事件。而利用冒泡方案由於是給ul綁的事件,無需再次繫結。

五. M$模型的麻煩

在M$模型中, 沒有對e.currentTarget的支援, 更糟糕的是, this也不指向當前的HTML元素。

(文章純屬個人備忘記錄用途,部分引用來自網上加上個人理解整理。歡迎轉載,請註明出處。如對你有幫助,請隨意打賞。)

JS中的事件順序(事件捕獲與冒泡)

相關文章