前言
一開始學到這一部分的時候,因為做的作品都很小,沒有用到這一部分的觀念,所以很快速的就帶了過去,但隨著開發經驗增加,發現機制的廣泛性,所以回來從新學了一次。記下這個筆記,梳理自己的邏輯。
這個理論要解決甚麼問題?
所有的理論都是起源自某個問題,在學習理論之前,必須要先了解它要解決甚麼問題,比較容易參透,所以我們就先從面對問題開始吧:
HTML 的元素都是由母元素包住子元素,層層交疊,在畫面上的結構就像下面的範例:
- 簡化版程式碼
<html> <head> </head> <body> <div class="Element1"> <div class="Element2"> <div class="Element3"> </div> </div> </div> </body> </html>
所以呢,假如我點按了最內層的 Element3 就等於同時點按了包住 Element3 的上層元素 Element2 和 Element1。(這邊可以將 Elemnet3 想像成家裡鋪好的木頭地板,Element2 和 Element1 則是底下的水泥層和泥土層,當你踩上木頭地板時,你也踩上了底下的水泥層和泥土層。)
有了這樣的概念後,我們進一步看這樣的原理會造成甚麼問題吧!
假如我在 Element3、Element2 以及 Element1 都設置了一個監聽 click 事件的 evnetListner。在這樣的設置下,當我點按 Element3 時等於三個元素同時都被點按到,而這三個 Element都有設置 eventListener,究竟哪一個 Element 的 event handler 可以優先被啟動呢?這也是bubbling 和 capturing 所要解決的主要問題:「執行順序」。
解決方式
解決執行順序問題的兩種方式
在古代,對於這個問題的解決方式因為瀏覽器以及公司不同,而有不同的排序方式:
capturing:
根據這個規則,瀏覽器會從根元素開始,一層層檢查是否有元素裝有 event handler,如果有的話就執行該 event handler,沒有的話就繼續往下找,直到到真正被點到的元素為止。依照這個規則,假如我點按了 <div class="Element3">
跑起來的實際順序會是長這樣:
Window > Document > html > body > div(".element1") > div(".element2") > div(".element3")
換言之,Element1 的事件會先被觸發,再來是 Element2 最後才是 Element3。
bubbling:
根據這個規則,瀏覽器為先從被實際點按的元素開始往上一層層檢查,是否有任何的元素有裝設該事件的 event handler,有的話就執行,沒有的話就繼續往上找,直到根元素為止。依照這個規則,跑起來的實際順序會是長這樣:
div(".element3") > div(".element2") > div(".element3") > body > html > Document > Window
換言之,Element3 的事件會先被觸發,再來是 Element2 最後才是 Element1。
W3C規範
把兩個組起來吧!
因為沒有統一的標準,各家瀏覽器各自選擇採納不同的標準,造成開發上很多的小問題,因此,W3C 最後制定了一個通用的標準: 大家別再吵了,我們 capturing 和 bubbling 通通都要可以用,由 developer 自行決定要用哪個吧!
組起來後怎麼跑?
其實這個新規則很單純,就是將已存在的兩種規則 capturing 以及 bubbling 組裝在一起,拆成 capturing phase 、target phase、bubbling phase,三個階段。整個流程會長成這樣:
- 依照capturing 的方式往下傳遞(capturing phase)
- 停留在實際被點擊的目標元素(target phase)
- bubbling 的方式往上傳遞(bubbling phase)
以這張圖為例,當目標元素 td 被點擊後:
- 瀏覽器會先依 capturing 的規則,從根元素開始一層層往下找。
- 直到實際被點擊的目標元素(在這張圖的例子裏就是td),停留。
- 再依照 bubbling 的規則從目標元素 td 往上一層層找直到根元素。
這個機制造成的影響
再了解完整個大架構大概是怎麼跑之後,let's work on 在這個大架構底下的重要觀念!
addEventListener 與 capturing、bubbling
從 W3C 制定了標準之後,developer 可以自由決定 element 的 event handler 要在 capturing 的階段還是 bubbling 的階段啟動。話雖如此,在實務上究竟要怎麼做呢?在這邊以原生的 DOM 方法做實際演練:
這是我們常用的 addEventListener()
e1ement.addEventListener("click", function(){
console.log("Guten Tag");
});
實際上,除了第一個參數事件類型、第二個參數 event handler以外,其實還有第三個參數可以傳入。
第三個參數是一個 Boolean ,true 代表 event handler要在 capturing 的階段被啟動;而 false 則代表 event handler 要在 bubbling 的階段被啟動。
e1ement.addEventListener("click", function(){
console.log("element1")
}, true);
這時你可能會好奇,那之前沒有特別設定時,就沒有 capturing 和 bubbling 了嗎?
答案是否定的 !
在 DOM 事件的世界裡,只要有事件的發生,capturing 和 bubbling 就會發生。所以呢,假如在 addEventListener 裡沒有特別設定,它也會預設為 bubbling。
1. 用 addEventListener() 來看看整個機制吧
首先,先寫一個簡單的 HTML:
<html>
<head>
</head>
<body>
<div class="Element1">
<div class="Element2">
<div class="Element3">
</div>
</div>
</div>
</body>
</html>
畫面結構大概長這樣:
再來,幫每個元素加上 event listener:
<script>
let e1 = document.querySelector(".Element1");
let e2 = document.querySelector(".Element2");
let e3 = document.querySelector(".Element3");
e1.addEventListener("click", function(){
console.log("element1 capturing")
}, true);
e1.addEventListener("click", function(){
console.log("element1 bubbling")
}, false);
e2.addEventListener("click", function(){
console.log("element2 capturing")
}, true);
e2.addEventListener("click", function(){
console.log("element2 bubbling")
}, false);
e3.addEventListener("click", function(){
console.log("element3 capturing")
}, true);
e3.addEventListener("click", function(){
console.log("element3 bubbling")
}, false);
</script>
在我們 click element3 後會依序跑出:
- element1 capturing
- element2 capturing
- element3 capturing
- element3 bubbling
- element2 bubbling
- element1 bubbling
因為不管如何都會按照 capturing 和 bubbling 的機制去跑,所以呢就算調換程式碼的排列順序,最後還是會依照上面的順序一個個被觸發並跑出結果。
但這邊有個例外,假如更改的是實際被點按的目標元素,得出來的結果會不一樣,那是因為,當這個機制跑到目標元素時等於進入了 target phase,而在 target phase 並沒有分 capturing 和 bubbling。因此,如果調換這邊的程式碼,結果是會改變的!
根據以上的行為在可以做個小總結:
- 這個機制會先捕獲再冒泡。
- 當傳遞到實際被點按的 target element 時,就等於跑到了 target phase,既不屬於 capturing 也不屬於 bubbling。
2. 怎麼中止事件的傳遞
stopPropagation()
addEventListener 於事件被觸發時會產生一筆有關於此事件的資料,並且可以當作參數傳進 event handler,以供利用。(可以把 function 想像成實際到現場執行的基層人員,總要有相關資料才知道怎麼做吧)。
這個參數不單提供資訊,也提供了一些方法,例如我們可以藉這個參數使用 stopPropagation(),來停止這個事件的傳遞。
element.addEventListener("click", function(e){
e.stopPropagation()
})
所以在 bubbling and capturing 在實作上有甚麼用呢?
事件代理(event delegation)
事件代理的原理很單純,在前面我們了解到,不管我們點按甚麼元素到最後一定都會 bubbling 回去 parent element, 因為這樣的機制存在,我們可以利用 parent element 來代理接收事件、並處理相對應的 event handler,單用文字可能不好理解,接下來用以下的例子來時作一次,會更容易清楚:
假如現在我有一個 ul element,裡面放三個 li element,並且之後可能會隨著需求增加 il element 的數量:
<ul class="list">
<li class="list_item">1</li>
<li class="list_item2">2</li>
<li class="list_item3">3</li>
</ul>
除此之外,我希望在點擊各個 li element 的時候,都會 log 出各 li element 裡的 text,這時候我們可以在一個一個幫 li element 加上 EventListner 來監聽事件:
<script>
let list_item
= document.querySelector(".list_item");
list_item.addEventListener(
"click",
function(){
let content = list_ite,.textContent;
console.log(content);
})
</script>
但是如果 li element 很多的話,這樣手動一個個加上去,其實會花上許多的時間,因此,在這裡我們可以利用 capturing 以及 bubbling 的特性,用 ul 來監聽事件,這樣不管加幾個 li,因為他們始終都是被身為 parent element 的 ul 所包住的,所以在 每個 li 上面所發生的事件 ul 也同樣可以接收的到:
利用這樣的特性,再加上 addEventListener 監聽事件所製成,並且傳進 event handler 當參數的 event data,我們就可以把 event listener 裝在 parent element,再由 event handler 去判斷要怎麼執行:
<script>
let list = document.querySelector(".list");
list.addEventListener("click", function(e){
let target = e.target;//找到被點按的那個 li
let text = target.textContent;
console.log(text);
})
</script>
這樣做的話就不需要在一個個幫每個 li 裝上 event listener,不單省下許多功夫,也讓整個程式碼的編排更 dry、更易讀!
總結
這篇是我第一篇詳實紀錄的筆記,希望跟我一樣的初學者,在看這篇文章後會更容易理解 capturing 與 bubbling 的機制。
當然,畢竟是一篇初學者所寫的文章,如果有任何不足的地方,也請讀者不另指教!