別人說不難,但我怎麼覺得有點難。
把不懂的地方寫下來提醒自己
我會搞混的點應該是因為沒有搞清楚這件事:一個 addEventListender
只可以監聽「一個階段」
在預設的情況下 addEventListender
監聽的是「冒泡階段」,所以你絕對不會看到外面的元素比裡面的元素還要早被觸發,因為大家都是監聽冒泡階段。也就是說外面的元素一定要等到 Target Phase 結束之後,進到冒泡階段往上傳才會一個一個被觸發。
那你要怎麼讓外面的元素先被觸發?讓它監聽捕獲階段
大概簡單來說就是這樣子吧…
事件的傳遞流程
事件的傳遞機制其實分成「冒泡」跟「捕獲」,參考這張圖:
意思是說,當你點下 <td>
時,事件會先從 window
一路傳下來,接著到了 <td>
後,在一路傳回去。
- 從
window
到 <td>
的這一段叫做「捕獲階段(Capture phase)」
- 在
<td>
到 window
的這一段叫做「冒泡階段(Bubbling Phase)」
- 至於「Target Phase」就是觸發事件的那個元素,因為它是目標,所以沒有分冒泡或捕獲
所以從上圖可以知道事件的傳遞機制是:先捕獲,在冒泡
我們可以來實作看看就知道了:
1 2 3 4 5
| <div class="outter"> <div class="inner"> <button class="btn">btn</button> </div> </div>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| function addEventCapturing(selector) { document.querySelector(selector) .addEventListener('click', function(e) { console.log(selector, '捕獲') }, true) }
function addEventBubbling(selector) { document.querySelector(selector) .addEventListener('click', function(e) { console.log(selector, '冒泡') }, false) }
addEventBubbling('.btn') addEventBubbling('.inner') addEventCapturing('.outter')
|
刻意設成這樣的用意是為了讓你理解,只要讓外面的元素監聽「捕獲」,它就可以比裡面早被觸發。
Output:
1 2 3
| .outter 捕獲 .btn 冒泡 .inner 冒泡
|
接著讓每個元素都同時監聽「捕獲」與「冒泡」階段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| function addEventBoth(selector) { document.querySelector(selector) .addEventListener('click', function() { console.log(selector, '冒泡') }, false) document.querySelector(selector) .addEventListener('click', function() { console.log(selector, '捕獲') }, true) }
addEventBoth('.btn') addEventBoth('.inner') addEventBoth('.outter')
|
Output:
1 2 3 4 5 6
| .outter 捕獲 .inner 捕獲 .btn 捕獲 .btn 冒泡 .inner 冒泡 .outter 冒泡
|
補充-在 Target Phase 中的捕獲與冒泡
[FE102] 中提到,如果在 Target Phase 同時監聽捕獲與冒泡階段,這時候會根據 addEventListener
的「設定順序」來執行,因此照這個邏輯底下應該要輸出:冒泡 -> 捕獲
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| function addEventBoth(selector) { document.querySelector(selector) .addEventListener('click', function() { console.log(selector, '冒泡') }, false) document.querySelector(selector) .addEventListener('click', function() { console.log(selector, '捕獲') }, true) }
addEventBoth('.btn')
|
但實測發現這部分似乎有被修正過,所以一樣會遵守「先捕獲,在冒泡」的這個原則: