JavaScript - Event bubbling and capturing (事件冒泡與捕獲)

參考來源一
參考來源二

事件機制

JavaScript 是「事件驅動」的程式語言,雖然瀏覽器在載入網頁後就會讀取 JavaScript 相關程式碼,但只有等到「事件」發生 (像是使用者滑鼠點擊、按下鍵盤)才會執行相關程式。

以網頁對話框為例:按下開啟 (觸發事件)-> 彈出對話框 (執行相關事件)

這整個對話框從觸發到開啟的過程牽涉到 DOM 的事件流程,就是「網頁元素接收事件的順序」

事件流程 (event flow)

由於 inner 也是 outer 的一部分,所以點擊 inner 的時候,也點到 outer(實際上也點擊整個網頁)。假如我在兩個元素上面都加了 eventListener,知道哪個先執行就很重要。

1
2
3
<div id="outer">
<div id="inner"></div>
</div>

事件的三個階段

事件流程可以分成三個階段

event phase 定義

1
2
3
CAPTURING_PHASE                = 1;
AT_TARGET = 2;
BUBBLING_PHASE = 3;
  • 事件冒泡 (event bubbling):
    事件冒泡是從「啟動事件的元素節點」開始,逐層往上傳遞」直到網頁的根節點,也就是 document。
1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html>
<head>
<title> </title>
</head>
<body>
<div>
click
</div>
</body>
</html>

在事件冒泡的觸發順序會是:

  1. click
  2. body
  3. html
  4. document
  • 事件捕獲 (event capturing)
    事件捕獲的機制正好相反,觸發是順序會從根節點開始
    在事件冒泡的觸發順序會是:
  1. document
  2. html
  3. body
  4. click
  • AT_TARGET

DOM 事件會從根節點往下傳遞到 target,target 就是點擊的目標,如果在 target 上加上事件監聽,他就會處在 AT_TARGET 這一個 Phase。

先捕獲再冒泡

事件究竟依賴哪種機制執行?兩種都會。在點擊 td 的時候,會從 window 開始往下傳,一直傳到 td 為止,到這邊就叫做 CAPTURING_PHASE,捕獲階段。接著事件傳遞到 td 本身,這時候叫做 AT_TARGET。最後事件會從 td 一路傳回去 window,這時候叫做 BUBBLING_PHASE,冒泡階段

如何決定要在捕獲還是冒泡階段監聽這個事件? 這裡可利用 addEventListener 的第三個參數,true 代表把這個 listener 添加到捕獲階段,false 或是沒有傳就代表把這個 listener 添加到冒泡階段。

1
target.addEventListener(type, listener, useCapture);

以實際例子看:

1
2
3
4
5
6
<div>
<div id="parent">
父元素
<div id="child">子元素</div>
</div>
</div>

幫每一個元素的每一個階段都添加事件,並且利用事件物件(event object) 提供的資訊了解事件傳遞機制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var parent = document.querySelector("#parent");
var child = document.querySelector("#child");

//透過地三個參數 true/false 分別設定捕獲及冒泡機制

parent.addEventListener(
"click",
function (e) {
console.log("parent capturing", e.eventPhase);
},
true
);
parent.addEventListener(
"click",
function (e) {
console.log("parent bubbling", e.eventPhase);
},
false
);
child.addEventListener(
"click",
function (e) {
console.log("child capturing", e.eventPhase);
},
true
);
child.addEventListener(
"click",
function (e) {
console.log("child bubbling", e.eventPhase);
},
false
);

當我點擊子元素的時候,事件觸發在 console 輸出的結果如下:

1
2
3
4
parent capturing 1
child capturing 2
child bubbling 2
parent bubbling 3

點擊父元素的時候,則出現

1
2
parent capturing 2
parent bubbling 2

1 是 CAPTURING_PHASE,2 是 AT_TARGET,3 是 BUBBLING_PHASE。

很明顯看出,事件的確是從最上層一直傳遞到 target。當事件傳遞到點擊的真正對象,也就是 e.target 的時候,無論你使用 addEventListener 的第三個參數是 true 還是 false,這邊的 e.eventPhase 都會變成 AT_TARGET。

所以即便調換子層的 capturing 或 bubbling 順序,也沒有什麼捕獲跟冒泡之分,所以執行順序就會根據你 addEventListener 的順序而定,先添加的先執行,後添加的後執行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
child.addEventListener(
"click",
function (e) {
console.log("child bubbling", e.eventPhase);
},
false
);

child.addEventListener(
"click",
function (e) {
console.log("child capturing", e.eventPhase);
},
true
);

得到

1
2
3
4
parent capturing 1
child bubbling 2
child capturing 2
parent bubbling 3
事件的傳遞順序,只要記住兩個原則就好:
- 先捕獲,再冒泡
- 當事件傳到 target 本身,沒有分捕獲跟冒泡

Powered by Hexo and Hexo-theme-hiker

Copyright © 2013 - 2020 CYC'S BLOG All Rights Reserved.

UV : | PV :