[JavaScript] Javascript 中的 DOM 事件傳遞機制:捕獲與冒泡 (capturing and bubbling)

itsems
itsems_frontend
Published in
10 min readJul 26, 2020
Photo by Markus Spiske on Unsplash

Javascript 是一種事件驅動 (Event-driven) 的程式語言,簡單來說就是當 user 對網頁做了一些動作 (ex: 點擊click),才會觸發執行要做的動作。

你知道嗎?當你點擊了ul li 裡面的一個 a link,其實 ul, li, a 都被點到了,因為 ul 包著 li,而 li 包著 a ,所以實際上來說,你點到了整個網頁,這是什麼意思?

這就是 Javascript 中的 DOM 事件傳遞機制:「傳遞與冒泡」

為了證明上述所說,我在這三個元素上綁定 addEventListener 事件印出 console:

如圖所見,在事件上發生的順序,會先觸發了我所點擊的 a link,再依序往回觸發父層 DOM 元素的事件。

事件傳遞機制總共分為三大階段:

  • 捕獲階段 (Capture Phase)
  • 目標階段 (Target Phase)
  • 冒泡階段 (Bubbling Phase)

W3 的圖很清楚,事件階段 (eventPhase) 如何定義也可以在這邊看到:

// PhaseType
const unsigned short CAPTURING_PHASE = 1;
const unsigned short AT_TARGET = 2;
const unsigned short BUBBLING_PHASE = 3;

捕獲階段 (Capture Phase)

在捕獲階段,DOM 的事件會從祖先層 (window) 開始往下尋找目標 (target),這個過稱稱為捕獲階段 (CAPTURING_PHASE)。

目標階段 (Target Phase)

在找到目標的時候,就會是目標階段 (AT_TARGET)。

冒泡階段 (Bubbling Phase)

循著原路回去的過程,就是冒泡階段 (BUBBLING_PHASE)。

這個階段的順序,也是常常聽到的口訣:「先捕獲,再冒泡」

所以我的 addEventListener 就是在冒泡階段依序觸發的囉?也不完全是。其實我們所熟知的 addEventListener 還有第三個參數:true/false 可以決定你的監聽要在捕獲階段或是冒泡階段觸發:

  • 預設為 false ,則為監聽冒泡階段
  • 若改為 true ,就是監聽捕獲階段

來看看剛剛的例子中,我的監聽事件就如平常的方式一樣:

如果我在每一個 addEventListener 加上了 true 的參數,表示我要在捕獲階段就觸發我的動作:

因為捕獲階段是從父層一層一層找下去的,所以觸發的順序跟剛剛相反了。

再看一眼剛剛的事件階段 (eventPhase) 定義:

CAPTURING_PHASE = 1 // 捕獲階段:1
AT_TARGET = 2 // 目標階段:2
BUBBLING_PHASE = 3 // 冒泡階段:3

把這個 eventPhase 加入剛剛的範例中,來看看我們是不是真的在捕獲階段觸發了我們的動作:

如我們的期待,的確是在捕獲階段觸發了父層的事件,也在找到點擊的 alink 的時候成為目標階段。如果我們再把剛剛沒有設定第三個參數,預設為 false 的 addEventListener 加回去的話,照理說就可以看到整個捕獲與冒泡的過程:

ul.addEventListener("click", function (e) {
console.log("ul clicked", e.eventPhase);
}, true);
li.addEventListener("click", function (e) {
console.log("li clicked", e.eventPhase);
}, true);
alink.addEventListener("click", function (e) {
console.log("a link clicked", e.eventPhase);
}, true);
ul.addEventListener("click", function (e) {
console.log("ul clicked", e.eventPhase);
});
li.addEventListener("click", function (e) {
console.log("li clicked", e.eventPhase);
});
alink.addEventListener("click", function (e) {
console.log("a link clicked", e.eventPhase);
});

結果:

跟想像中的差不多,但是在目標階段這邊有一個小小的地方要注意,就是在 target phase 的時候,不論你的 addEventListener 設為 true/false 都沒有差別,可以把 target phase 想像成一個點,捕獲和冒泡想像成一個過程,在 target phase 這個位置,就不會管 addEventListener 的 true/false,只照著程式的順序執行。

舉例:

alink.addEventListener("click", function (e) {
console.log("a link clicked when bubbling", e.eventPhase);
});
alink.addEventListener( "click", function (e) {
console.log("a link clicked when capturing", e.eventPhase);
}, true);

我寫了兩個 alink 的監聽,第一個監聽為預設是 false,應該會在冒泡階段執行,第二個則為 true,應該要在捕獲階段執行,執行結果卻會是:

表示上述所說,addEventListener 的 true/false 並不影響 target phase 觸發的階段,他不算是捕獲或是冒泡的階段之一。

動手玩玩看:

停止事件傳遞 (stopPropagation) vs 取消預設行為 (preventDefault)

停止事件傳遞就是阻止 DOM 再往下一個節點繼續捕獲或是冒泡事件,常見的使用時機是,如果我們點了裡面的 <div> ,不要讓他也觸發了外部 <div> 的事件,舉例:

我有一個 #outer 的 div,裡面包了一個 #inner 的 div,如果我點了 #inner 的 div,#outer 的 div 事件也會跟著觸發:

我想要我點擊 inner 的時候,只觸發 inner 的事件,這種時候就可以用上 stopPropagation()

inner.addEventListener("click", function (e) {
console.log("inner clicked");
e.stopPropagation();
});

在這邊的 addEventListener 我沒有特別設定 true/false,表示預設為 false,即讓他中斷冒泡階段的傳遞,如果我加上了 true,嘗試讓他中斷捕獲的傳遞,可以像是這樣:

outer.addEventListener("click", function (e) {
console.log("outer clicked");
e.stopPropagation();
}, true);

這樣的意思就是,我在點擊 outer 的時候,中斷往下傳遞的行為,所以當我點擊 outer 所包住的 inner 時,inner 的行為就不會被觸發:

stopPropagation() 常常與 preventDefault() 混為一談,但是其實後者的作用為取消瀏覽器預設行為,什麼是預設行為?我點了 a link,超連結帶我去了別的網頁,就是預設行為。如果我在點擊 a link 加設了 preventDefault() ,則此預設行為被取消:

link.addEventListener("click", function (e) {
e.preventDefault();
});

動手玩玩看:

如果上述的捕獲與冒泡機制你已經理解了,那就會明白取消預設行為和事件傳遞機制是不一樣的事情,如果在這之前你不是特別清楚,希望這篇文章可以讓你更加理解他們之間的差異。

📝 Quiz time !

以下取自 JavaScript Questions 中相關的小測驗,希望看完上面的說明,下面的題目可以讓你迎刃而解:

What are the three phases of event propagation?

  • A: Target > Capturing > Bubbling
  • B: Bubbling > Target > Capturing
  • C: Target > Bubbling > Capturing
  • D: Capturing > Target > Bubbling
Photo by Joakim Honkasalo on Unsplash
— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —— — — — — — — — — — — 仔細想想,不要偷看👩‍🏫 — — — — — — — — — — — — — —— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — —

Answer: D

先捕獲,再冒泡!在捕獲(capturing)階段中,事件從祖先元素向下傳播到目標元素。當事件到達目標(target)元素後,冒泡(bubbling)才開始。

內容若有任何錯誤,歡迎留言交流指教! 🐬

延伸閱讀:
重新認識 JavaScript: Day 14 事件機制的原理
DOM 的事件傳遞機制:捕獲與冒泡

--

--

itsems
itsems_frontend

Stay Close to Anything that Makes You Glad You are Alive.