我們在設計網頁時經常需要為頁面添加各種效果與功能,例如按鍵反應、頁面元素特效等,這些都是透過添加事件功能來達成,但你有沒有深入了解這些事件背後的機制呢?今天讓我們由下面的問題來了解JavaScript的事件機制吧~
- 事件機制是什麼?
- 事件是如何傳遞的?
- 如何區分捕獲與冒泡?
- 如何取消事件傳遞?
事件機制是什麼?
JavaScript是事件驅動(Event-driven)的程式語言,也就是當使用者在網頁上觸發了一些行為,才會執行相關的程式動作。
例如:使用者在網頁上點擊了一個按鍵,然後畫面出現了一個alert警告。
這個點擊按鍵的行為便是事件,每個可用的事件都會有一個事件處理器,也就是事件觸發時執行的程式。
接下來讓我們看事件的背後到底做了哪些事?
事件是如何傳遞的?
事件流程 Event Flow
這邊引用W3的事件傳遞示意圖。
當我們點擊畫面table的其中一個td時,在點擊的事件到達實際目標的td之前,會依序觸發每個父元素的點擊事件,就是點擊td也等同於點擊了整個table,以此類推等於點擊了整個網頁。
這便是事件流程 (Event Flow),也就是網頁元素接收事件的順序。
事件傳遞的3個階段
既然~那麼了解事件傳遞的方式就很重要,也是本文的重點。
事件的傳遞的3個階段
- 捕獲階段(capture phase)
- 目標階段(target phase)
- 冒泡階段(bubbing phase)
接下來分別介紹各個事件階段。
捕獲階段(capture phase)
當點擊td時,這個點擊事件從window開始往下傳遞,直到目標td為止。便是捕獲階段(capture phase)
- Document
- <html>
- <body>
- <table>
- <tbody>
- <tr>
- <td>(target)
冒泡階段(bubbing phase)
冒泡階段(bubbing phase)與捕獲階段(capture phase)相反,事件由目標td開始一路傳回window的過程。
- <td>(target)
- <tr>
- <tbody>
- <table>
- <body>
- <html>
- Document
目標階段(target phase)
當事件傳遞到目標td時,便是目標階段(target phase)。
這個階段有個要注意的點,過往許多文章有提到在目標階段捕獲事件與冒泡事件會依照程式的順序執行。
再撰寫本文當下,最新版本的Chrome與Edge瀏覽器改以先捕獲再冒泡的原則,而Firefox還是依照程式順序執行,不清楚Firefox未來是否會跟進。
至2022-6-17 瀏覽器版本:
- Chrome:102.0.5005.115
- Edge:102.0.1245.44
- FireFox:101.0.1
在目標階段(target phase)同時存在捕獲事件與冒泡事件時,如果這時監聽事件的話,監聽的事件會依照程式的順序執行。
這部分可以參考:Chrome 89 更新事件触发顺序,导致99%的文章都错了(包括MDN)
執行程式:
<div>
<button id="test_btn">click me!</button>
</div>
const test_btn = document.querySelector("#test_btn")
test_btn.addEventListener("click", (e) => {
console.log("button bubbling", e.eventPhase);
}, false);
test_btn.addEventListener("click", (e) => {
console.log("button capturing", e.eventPhase);
}, true);
這邊附上測試結果:
事件傳遞的原則:
口訣:先捕獲,再冒泡
監聽事件器addEventlistener
事件監聽器(addEventlistener)在實務上經常用來為元素綁定事件功能,如加入click、change、submit等。
除了上述的主要功用,事件監聽器還可以在參數中設定要在事件傳遞的哪個階段監聽事件。
addEventlistener的參數
addEventListener()有3個參數可以使用,分別是:
- 事件名稱-例如click與change,更多可以參考MDN
- 事件處理器-事件觸發時執行的函式(function)
- 決定事件是以捕獲還是冒泡機制的Boolean值
第三個參數
事件監聽器的第三個變數參數填入true,該事件監聽於捕獲階段。
如果填入false或是沒有填入,該事件監聽於冒泡階段。
如何取消事件傳遞?
既然可以分別監聽事件流程的各個階段,那麼是否可以中斷這個傳遞過程呢?當然有,這個方法便是event.stopPropagation。
當event.stopPropagation加在哪邊,事件傳遞便中斷在哪,並且不會繼續往後傳遞。
讓我們從例子來看:
<ul id="ul_1">
<li id="li_1">
<button id="btn_1">click me!</button>
</li>
</ul>
const getDom = (id) => document.getElementById(id);
const ul_1 = getDom("ul_1");
const li_1 = getDom("li_1");
const btn_1 = getDom("btn_1");
// capture phase
ul_1.addEventListener("click", (e) => {
console.log("ul_1 capturing", e.eventPhase);
}, true);
li_1.addEventListener("click", (e) => {
console.log("li_1 capturing", e.eventPhase);
e.stopPropagation();// 在li中斷事件傳遞
}, true);
btn_1.addEventListener("click", (e) => {
console.log("btn_1 capturing", e.eventPhase);// 沒有執行
}, true);
上面的例子中,在li的捕獲階段插入event.stopPropagation讓事件傳遞在這裡中斷,事件中斷同時也代表後續的事件行為會被取消。所以按鍵的捕獲階段的事件便不會印出結果。
event.stopPropagation與event.preventDefault的差別
- event.stopPropagation:功能在上面有先介紹了,便是取消事件的傳遞。
- event.preventDefault:停止瀏覽器的預設行為,但事件會繼續向後傳遞。
event.preventDefault
preventDefault的功用是停止瀏覽器的預設行為。以最常見的a link來說,當點擊了a link便會連結到其他網站,這便是預設行為。
如果在點擊a link時加入preventDefault,便會取消超連結的預設行為。
這邊修改下上面的例子:
<ul id="ul_1">
<li id="li_1">
<!-- <button id="btn_1">click me!</button> -->
<a id="a_1" target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault">to MDN
preventDefault</a>
</li>
</ul>
const getDom = (id) => document.getElementById(id);
const ul_1 = getDom("ul_1");
const li_1 = getDom("li_1");
const a_1 = getDom("a_1");
// capture phase
ul_1.addEventListener("click", (e) => {
console.log("ul_1 capturing", e.eventPhase);
}, true);
li_1.addEventListener("click", (e) => {
console.log("li_1 capturing", e.eventPhase);
// e.stopPropagation();
}, true);
a_1.addEventListener("click", (e) => {
e.preventDefault(); // 停止<a>的預設行為
console.log("a_1 capturing", e.eventPhase);
}, true);
// bubbling phase
ul_1.addEventListener("click", (e) => {
console.log("ul_1 bubbling", e.eventPhase);
}, false);
li_1.addEventListener("click", (e) => {
console.log("li_1 bubbling", e.eventPhase);
}, false);
上面的例子可以看到a link的預設行為被取消了,所以無法連結到對應的網頁。
這邊要特別注意,雖然我們停止了a link的行為,但事件的傳遞依然會持續進行,所以依然會印出各個事件階段的情形。
事件委託(Event Delegation)能做什麼?
事件的捕獲與冒泡可以幫助我們在處理事件時有更大的使用彈性,將事件監聽與事件流程進一步延伸,便是事件委託(Event Delegation)。
事件委託(Event Delegation)的概念是利用事件傳遞的機制,在元素的共同外層綁定事件(addEventListener)。
如此一來便不需為每個元素分別綁定事件,而是由外層元素管理底下多個元素的事件。
這個例子中要為每個按鍵加入hover效果。
<div id="menu">
<button>購物車</button>
<button">結帳</button>
<button>取消</button>
</div>
透過事件委託的概念,我們在外層的div綁定事件監聽器處理內層按鍵的hover效果。
const menu = document.getElementById("menu");
menu.addEventListener("mouseover", (e) => {
if (e.target.tagName.toLowerCase() !== 'button') return;
e.target.style.color = "red";
});
menu.addEventListener("mouseout", (e) => {
if (e.target.tagName.toLowerCase() !== 'button') return;
e.target.style.color = "";
});
這樣做為我們帶來了幾個好處:
使用更少的程式管理需要的功能
如果將事件綁定在各個按鍵上,上面的例子便要使用6個監聽器來完成hover的效果。
當要更動按鍵效果時就不好處理,而事件委託解決了這個問題。
可以動態管理內層元素的事件
上面例子中目前只有3個按鍵,如果需要動態新增按鍵,事件委託便可以幫我們更輕易管理。
// 加入新按鈕
const creat_btn = document.createElement("button");
creat_btn.textContent = "new_btn";
menu.appendChild(creat_btn);
由上面的程式加入新按鍵。
加入的按鍵無須再綁定事件監聽器就具有hover的效果,透過事件委託便可以輕易批量控制元素的事件。
如果需要分別控制按鍵,可以為元素加入屬性,結果像下面這樣:
<div id="menu">
<button data-action="cart">購物車</button>
<button data-action="checkout">結帳</button>
<button data-action="cancel">取消</button>
</div>
menu.addEventListener("click", (e) => {
if (e.target.getAttribute('data-action') === 'checkout') {
alert(e.target.textContent); // 結帳
}
});
總結
總結一下這篇文章中我們獲得了什麼?
事件的傳遞的3個階段:
- 捕獲階段(capture phase) – 事件從window開始往下傳遞,直到目標為止
- 目標階段(target phase) – 同時存在捕獲事件與冒泡事件
- 冒泡階段(bubbing phase) – 事件由目標開始一路傳回window的過程
事件傳遞的原則:
- 先捕獲,再冒泡
event.stopPropagation與event.preventDefault的差別:
- event.stopPropagation:取消事件的傳遞。
- event.preventDefault:停止瀏覽器的預設行為,但事件會繼續向後傳遞。
事件委託(Event Delegation)
- 概念:利用事件傳遞的機制,在元素的共同外層綁定事件(addEventListener)。
- 使用更少的程式管理需要的功能
- 可以動態管理內層元素的事件
參考資料
JavaScript – The Complete Guide
MDN-Introduction to events
W3-event-flow
DOM 的事件傳遞機制:捕獲與冒泡
事件機制的原理
Event delegation
Chrome 89 更新事件触发顺序,导致99%的文章都错了(包括MDN)
如果對文章內容有任何問題或是錯誤,歡迎在底下留言讓我知道: )