React 事件處理與事件傳遞:一篇搞懂冒泡、捕獲與阻止事件!
前言:
在開發React 時,「事件」是我們最常打交道的部分之一。無論是點擊按鈕、提交表單,還是操作其他互動元素,都需要處理事件。如果你剛接觸 React,可能會有以下疑問:
- 為什麼點擊按鈕時會觸發父層的事件?
- 什麼是事件的「冒泡」和「捕獲」?
- 如何攔截事件傳播或避免預設行為?
這篇文章將帶你從零開始理解 React 的事件處理,並解釋事件傳遞的原理及其在開發中的實際應用場景。
React 的事件處理基礎
什麼是事件?
根據 @MDN Web Docs - Introduction to events 對於事件的定義,事件是瀏覽器用來回應用戶操作的一種訊號。例如當你點擊按鈕、鍵入文字、 或滑動滑鼠時,瀏覽器都會生成一個事件並傳遞給應用程式進行處理。
常見的事件類型包括:
- 滑鼠事件(Mouse Events):
click
,dblclick
,mousedown
,mouseup
,mousemove
,mouseover
,mouseout
- 鍵盤事件(Keyboard Events):
keydown
,keyup
,keypress
- 表單事件(Form Events):
submit
,change
,input
,focus
,blur
在 JavaScript 中,事件本身是一個由瀏覽器產生的事件物件(Event Object),它記錄了與互動相關的所有資訊,例如觸發的 HTML 元素、滑鼠座標、鍵盤按鍵等。
讓我們看看事件物件的基本樣貌和幾個常用屬性:
document.querySelector('button').addEventListener('click', (event) => {
console.log(event.type); // 事件類型,例如 "click"
console.log(event.target); // 觸發事件的 DOM 元素
console.log(event.currentTarget); // 綁定事件的 DOM 元素
console.log(event.clientX, event.clientY); // 滑鼠的 X、Y 座標
});
HTML/JS 的事件處理方式
在早期的網頁開發中,事件處理主要有以下幾種方式:
-
內聯屬性(Inline Attributes)
直接在 HTML 元素上綁定事件處理邏輯,這種方式直觀但不推薦,因為不易維護。
<button onclick="alert('Hello!')">點擊我</button>
-
使用 DOM 的
addEventListener
更現代且推薦的方式是使用 JavaScript 的
addEventListener
,可以將事件邏輯與 HTML 結構分離,提升可讀性和重用性。const button = document.querySelector('button');
button.addEventListener('click', () => {
alert('Hello!');
});
React 的事件處理方式
React 的事件處理與原生 HTML 有顯著的不同,主要表現在以下幾點:
-
使用合成事件(Synthetic Event)
在 React 中,事件處理是基於一種叫做 合成事件(Synthetic Event) 的機制。合成事件是一層跨瀏覽器的事件封裝,提供了一個一致的介面。React 不會直接將事件綁定到每個 DOM 元素,而是採用事件委派的方式,將所有事件統一綁定到根節點(例如
document
或root
元素)。這樣做的好處是:
- 跨瀏覽器兼容性:自動處理不同瀏覽器之間的事件差異。
- 效能提升:React 使用事件委派機制,將所有事件綁定在根元素上,減少 DOM 操作。
function handleClick(event) {
console.log(event.type); // "click"
}
<button onClick={handleClick}>點我</button> -
駝峰式命名
在 React 中,事件屬性使用駝峰式命名(Camel Case),例如
onClick
、onMouseOver
。 -
事件處理程式是函式引用
React 要求事件處理程式是函式的引用,而不是直接執行結果。這樣可以避免函式在渲染時被過早執行 。
錯誤示範:
function handleClick() {
alert('點擊了');
}
<button onClick={handleClick()}>點我</button> // handleClick 立即執行,錯誤!正確寫法:
<button onClick={handleClick}>點我</button>
什麼是事件傳遞?捕獲與冒泡解讀
在 JavaScript 事件模型中,當事件觸發時,並不僅僅作用於目標元素本身,還會按照一個固定的順序傳遞。這種事件傳遞機制分為兩個主要階段:捕獲階段 和 冒泡階段。
事件傳遞的階段
事件在傳遞過程中會經歷以下三個階段:
-
捕獲階段(Capture Phase)
從
window
開始,沿著 DOM 樹向下傳遞到事件的目標元素。 -
目標階段(Target Phase)
事件到達目標元素,並執行與該元素相關的事件處理程式。
-
冒泡階段(Bubble Phase)
從目標元素開始,沿著 DOM 樹向上傳遞到
window
。
這樣的流程讓開發者可以在事件傳遞的不同階段執行邏輯。
冒泡階段(Bubble Phase)
冒泡指的是事件從目標元素向其祖先元素傳遞,直至 window
為止。當需要在父層統一處理子層的邏輯時,冒泡階段非常有用。
React 默認只處理冒泡階段的事件,
範例:父元素捕獲子元素的點擊事件
function handleParentClick(event) {
console.log('父層事件觸發');
console.log('點擊的子元素是:', event.target);
}
<div onClick={handleParentClick}>
<button>按鈕 1</button><button>按鈕 2</button>
</div>
輸出結果:
- 當點擊「按鈕 1」時,
event.target
是<button>
,但事件處理程式屬於<div>
。 - 這種行為利用了冒泡機制,可以讓父元素統一監聽子元素的點擊事件,適用於動態新增的元素。
捕獲階段(Capture Phase)
捕獲指的是事件從 window
開始向下傳遞,直到目標元素。當需要攔截某些特定事件時,捕獲階段可以優先處理。
前面提到,React 預設只處理 冒泡階段 的事件。如果我們需要在捕獲階段處理事件,可以在事件名稱後加上 Capture
,例如 onClickCapture
。
範例:捕獲事件的使用
function handleCapture(event) {
console.log('捕獲階段');
}
function handleBubble(event) {
console.log('冒泡階段');
}
<div onClickCapture={handleCapture} onClick={handleBubble}>
<button>點擊我</button>
</div>
執行順序:
handleCapture
執行(捕獲階段)。handleBubble
執行(冒泡階段)。
捕獲與冒泡的實際應用
看到這邊你可能會想:「我在日常開發中真的需要關心捕獲和冒泡嗎?不能一招 onClick 打天下嗎」答案是 視情況而定。以下我來舉一些常見的例子
冒泡階段的應用
冒泡階段特別適合用於以下場景:
1. 父元素統一監聽子元素的事件
這種模式常被稱為 事件委派(Event Delegation)。它能夠有效減少事件監聽器的數量,並支持動態新增或刪除子元素。
範例:列表項的點擊事件
假設我們有一個動態生成的列表,每個項目都需要響應點擊事件。可以在父層 <ul>
上添加事件監聽器,通過 event.target
獲取具體被點擊的項目。
function handleItemClick(event) {
console.log('點擊的項目是:', event.target.textContent);
}
<ul onClick={handleItemClick}>
<li>項目 1</li>
<li>項目 2</li>
<li>項目 3</li>
</ul>
2. 全局事件監聽
在某些情況下,我們需要監聽整個頁面中的某類事件。例如,只在點擊彈窗以外的頁面空白處時才隱藏彈窗。
範例:點擊空白處關閉彈窗
function Modal({ onClose }) {
function handleOutsideClick(event) {
if (event.target.id === 'modal-background') {
onClose(); // 僅在點擊背景時觸發
}
}
return (
<div id="modal-background" onClick={handleOutsideClick}>
<div className="modal-content">
<p>這是一個彈窗</p>
</div>
</div>
);
}