-
Notifications
You must be signed in to change notification settings - Fork 53
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
DOM 的事件傳遞機制:捕獲與冒泡 #21
Comments
写的很不错 |
您好,想請問一下 JavaScript 的變數命名規範, const $list = document.getElementById('list'); 好像變數都是以 $ 開頭居多。 |
不,不是 |
瞭解,謝謝您。 |
@KuoHsiangYu2 不是喔,我只是習慣如果變數裡面存的內容是一個 DOM 物件,前面加上 |
@aszx87410 原來如此,我瞭解了。 |
前言
2021-05-25 補充:文中所提到的在 target phase 會依照加上 event listener 的順序觸發,在新版的 Chrome 似乎更改了這個行為,請參考:Chrome 89 更新事件触发顺序,导致99%的文章都错了(包括MDN)
(補充:感謝 othree 前輩的指點,指出這其實是在講 DOM 裡面事件傳遞的順序,因此把標題以及內文修正,原標題為:JavaScript 的事件傳遞機制:捕獲與冒泡)
今天為大家帶來的內容是 DOM 裡面的事件傳遞機制,而與這些事件相關的程式碼,相信大家應該不太陌生,就是
addEventListener
,preventDefault
跟stopPropagation
。簡單來說,就是事件在 DOM 裡面傳遞的順序,以及你可以對這些事件做什麼。
為什麼會有「傳遞順序」這一詞呢?假設你有一個
ul
元素,底下有很多li
,代表不同的 item。當你點擊任何一個li
的時候,其實你也點擊了ul
,因為ul
把所有的li
都包住了。假如我在兩個元素上面都加了
eventListener
,哪一個會先執行?這時候呢,知道事件的執行順序就很重要。另外,由於某些瀏覽器(沒錯,我就是在說 IE)的機制比較不太一樣,因此那些東西我完全不會提到,有興趣的可以研究文末附的參考資料。
簡單範例
為了之後方便說明,我們先寫一個非常簡單的範例出來:
在這個範例裡面,就是最外層一個
ul
,再來li
,最後則是一個超連結。為了方便辨識,id 的取名也跟階層架構有關係。DOM 畫成圖大概是長這樣:
有了這一個簡單的 HTML 結構之後,就可以很清楚的說明 DOM 的事件傳遞機制了。
事件的三個 Phase
要幫一個 DOM 加上 click 的事件,你會這樣寫:
而這邊的
e
裡面就蘊含了許多這次事件的相關參數,其中有一個叫做eventPhase
,是一個數字,表示這個事件在哪一個 Phase 觸發。eventPhase
的定義可以在 DOM specification 裡面找到:這三個階段,就是我們今天的重點。
DOM 的事件在傳播時,會先從根節點開始往下傳遞到
target
,這邊你如果加上事件的話,就會處於CAPTURING_PHASE
,捕獲階段。target
就是你所點擊的那個目標,這時候在target
身上所加的eventListenr
會是AT_TARGET
這一個 Phase。最後,事件再往上從子節點一路逆向傳回去根節點,這時候就叫做
BUBBLING_PHASE
,也是大家比較熟知的冒泡階段。這邊用文字你可能會覺得霧煞煞,我直接引用一張 w3c 講 event flow 的圖,相信大家就清楚了。
你在點擊那一個
td
的時候,這一個點擊的事件會先從window
開始往下傳,一直傳到td
為止,到這邊就叫做CAPTURING_PHASE
,捕獲階段。接著事件傳遞到
td
本身,這時候叫做AT_TARGET
。最後事件會從
td
一路傳回去window
,這時候叫做BUBBLING_PHASE
,冒泡階段。所以,在看一些講事件機制的文章的時候,都會看到一個口訣:
就是這樣來的。
可是,我要怎麼決定我要在捕獲階段還是冒泡階段去監聽這個事件呢?
其實,一樣是用大家所熟悉的
addEventListener
,只是這函數其實有第三個參數,true
代表把這個 listener 添加到捕獲階段,false
或是沒有傳就代表把這個 listener 添加到冒泡階段。實際演練
大概知道事件的傳遞機制之後,我們拿上面寫好的那一個簡單範例來示範一下,一樣先附上事件傳遞的流程圖(假設我們點擊的對象是
#list_item_link
)接著,來試試看幫每一個元素的每一個階段都添加事件,看一看結果跟想像中的是否一樣:
點一下超連結,console 輸出以下結果:
1 是
CAPTURING_PHASE
,2 是AT_TARGET
,3 是BUBBLING_PHASE
。從這邊就可以很明顯看出,事件的確是從最上層一直傳遞到 target,而在這傳遞的過程裡,我們用
addEventListenr
的第三個參數把 listener 添加在CAPTURING_PHASE
。然後事件傳遞到我們點擊的超連結(
a#list_item_link
)本身,在這邊無論你使用addEventListener
的第三個參數是true
還是false
,這邊的e.eventPhase
都會變成AT_TARGET
。最後,再從 target 不斷冒泡傳回去,先傳到上一層的
#list_item
,再傳到上上層的#list
。先捕獲,再冒泡的小陷阱
既然是先捕獲,再冒泡,意思就是無論那些
addEventListener
的順序怎麼變,輸出的東西應該還是會一樣才對。我們把捕獲跟冒泡的順序對調,看一下輸出結果是否一樣。一樣點擊超連結,輸出的結果是:
可以發現一件神奇的事,那就是
list_item_link
居然是先執行了添加在冒泡階段的 listener,才執行捕獲階段的 listener。這是為什麼呢?
其實剛剛上面有提到,當事件傳遞到點擊的真正對象,也就是 e.target 的時候,無論你使用
addEventListener
的第三個參數是true
還是false
,這邊的e.eventPhase
都會變成AT_TARGET
。既然這邊已經變成
AT_TARGET
,自然就沒有什麼捕獲跟冒泡之分,所以執行順序就會根據你addEventListener
的順序而定,先添加的先執行,後添加的後執行。所以,這就是為什麼我們上面把捕獲跟冒泡的順序換了以後,會先出現
list_item_link bubbling
的原因。關於這些事件的傳遞順序,只要記住兩個原則就好:
jsbin 範例程式碼
取消事件傳遞
接著要講的是,這一串事件鏈這麼長,一定有方法可以中斷這一條鏈,讓事件的傳遞不再繼續。而這個方法相信大家應該都不陌生,就是:
e.stopPropagation
。你加在哪邊,事件的傳遞就斷在哪裡,不會繼續往下傳遞。
例如說以上面那個例子來講,假如我加在
#list
的捕獲階段:這樣子,console 就只會輸出:
因為事件的傳遞被停止,所以剩下的 listener 都不會再收到任何事件。
不過,在這邊依然有一個地方要特別注意。
這邊指的「事件傳遞被停止」,意思是說不會再把事件傳遞給「下一個節點」,但若是你在同一個節點上有不只一個 listener,還是會被執行到。
例如說:
輸出結果是:
儘管已經用
e.stopPropagation
,但對於同一個層級,剩下的 listener 還是會被執行到。若是你想要讓其他同一層級的 listener 也不要被執行,可以改用
e.stopImmediatePropagation();
例如說:
輸出結果是:
取消預設行為
常常有人搞不清楚
e.stopPropagation
跟e.preventDefault
的差別,前者我們剛剛已經說明了,就是取消事件繼續往下傳遞,而後者則是取消瀏覽器的預設行為。最常見的做法就是阻止超連結,例如說:
這樣子,當點擊超連結的時候,就不會執行原本預設的行為(新開分頁或是跳轉),而是沒有任何事情發生,這就是
preventDefault
的作用。所以呢,
preventDefault
跟 JavaScript 的事件傳遞「一點關係都沒有」,你加上這一行之後,事件還是會繼續往下傳遞。有一個特別值得注意的地方是 W3C 的文件裡面有寫到:
意思就是說一旦 call 了
preventDefault
,在之後傳遞下去的事件裡面也會有效果。我們來看一個範例:
我們在
#list
的捕獲事件裡面就先寫了e.preventDefault()
,而根據文件上面所說的,這個效果會在之後傳遞的事件裡面一直延續。因此,等之後事件傳遞到
#list_item_link
的時候,你會發現點超連結一樣沒反應。實際應用
知道了事件的傳遞機制、取消傳遞事件跟取消預設行為之後,在實際開發上有什麼用處呢?
最常見的用法其實就是事件代理(Delegation),例如說你今天有一個 ul,底下 1000 個 li,如果你幫每一個 li 都加上一個 eventListener,你就新建了 1000 個 function。
但我們剛剛已經知道,任何點擊 li 的事件其實都會傳到 ul 身上,因此我們可以在 ul 身上掛一個 listener 就好。
而這樣的好處是當你新增或是刪除一個 li 的時候,不用去處理跟那個元素相關的 listener,因為你的 listener 是放在 ul 身上。這樣透過父節點來處理子節點的事件,就叫做事件代理。
除此之外,我有想到幾個滿有趣的應用,大家可以參考看看。
例如說剛剛提到的
e.preventDefault()
,既然我們知道原理跟使用技巧,就可以這樣用:只要這樣一段程式碼,就可以把頁面上所有的元素停用,點了都沒有反應,像是
<a>
點了不會跳出超連結,<form>
按了submit
也沒用,而且因為阻止事件冒泡,所以其他的onClick
事件也都不會執行。或是,也可以這樣用:
利用事件傳遞機制的特性,在
window
上面使用捕獲,就能保證一定是第一個被執行的事件,你就可以在這個 function 裡面偵測頁面中每一個元素的點擊,可以傳回去做數據統計及分析。結論
DOM 的事件傳遞機制算是 JavaScript 眾多經典面試題裡面相對簡單很多的,只要能掌握事件傳遞的原則跟順序,其實就差不多了。
而
e.preventDefault
與e.stopPropagation
的差別在知道事件傳遞順序之後也大概能理解,前者就只是取消預設行為,跟事件傳遞沒有任何關係,後者則是讓事件不再往下傳遞。希望這篇能讓大家理解 DOM 的事件傳遞機制,如果有哪邊有講錯,也麻煩大家不吝指證,感謝。
參考資料(比較推薦後面那些原文資料):
The text was updated successfully, but these errors were encountered: