在設計畫面互動的時候,有時候需要撰寫一些事件處理,這邊將介紹其中的原理與在 React 中的一些小知識。

簡單介紹

要專業的處理 event 事件,就必須要了解擷取與冒泡事件,概念簡單但是特別重要。
根據上圖,我們可以看到事件處理主要分為三個階段。

  1. 擷取階段
  2. 目標階段
  3. 冒泡階段

如果你覺得太複雜的話,你可以想像你是一位路跑選手,window 是起點與終點,目標是轉則點,你的目標是蒐集事件,而事件又分為冒泡事件和擷取事件,如果他是擷取事件,那你應該是在抵達轉折點之前蒐集,如果是冒泡事件則是在轉折之後蒐集。

理解這個概念之後就可以開始認識擷取與冒泡了。

DOM 元素事件

addEventListener 的第二個參數可以控制這個參數是屬於擷取還是冒泡事件,如果標記這個事件屬於擷取事件,那麼這個 EventListener 就會在擷取階段被 trigger。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<form>
FORM
<div>
DIV
<p>P</p>
</div>
</form>

<script>
for (const element of document.querySelectorAll("*")) {
element.addEventListener(
"click",
(e) => alert(`Capturing: ${element.tagName}`),
true
);
element.addEventListener("click", (e) =>
alert(`Bubbling: ${element.tagName}`)
);
}
</script>

React 元素事件

DOM element 與 React element 的事件處理是兩個不一樣卻類似的東西。
主要差異在:

  1. 事件的名稱在 React 中都是 c,而在 HTML DOM 中則是小寫。
  2. 事件的值在 JSX 中是一個 function,而在 HTML DOM 中則是一個 string。

SyntheticEvent

React 中的 event 為根據 W3C 定義的 SyntheticEvent,它也包含了瀏覽器的原生事件,例如:stopPropagation。

camelCase

1
<button onClick={activateLasers}>Activate Lasers</button>

避免瀏覽器預設行為

DOM element 可以使用 return false 辦到,React 可以使用 preventDefault

1
2
3
4
5
6
7
8
9
10
11
12
function Form() {
function handleSubmit(e) {
e.preventDefault();
console.log("You clicked submit.");
}

return (
<form onSubmit={handleSubmit}>
<button type="submit">Submit</button>
</form>
);
}

冒泡事件

冒泡顧名思義就是泡泡冒上來的意思,因此是 bottom-up 的方式傳遞事件。
幾乎大部分的事件都是屬於冒泡事件,當一個元素被 trigger 時,自己本身的 event handler 會先觸發,再往上冒泡 trigger parent 的 event handler。

以下以 DOM event 的 onclick 為例解釋冒泡事件:

1
2
3
4
5
6
7
8
9
10
<!--
Bubbling: <p> -> <div> -> <form>
-->
<form onclick="alert('form')">
FORM
<div onclick="alert('div')">
DIV
<p onclick="alert('p')">P</p>
</div>
</form>

也就是往上冒泡的過程,甚至可以到最外層的 html,幾乎所有的 parent 都知道你在做什麼了,可是我不想讓 parent 知道我在做什麼
可以使用到 event.stopPropagation() 來停止任何的冒泡和擷取事件傳遞。
但是需要注意是在什麼階段使用它,才能夠妥善達到預期的效果。

擷取事件

在擷取階段的時候,擷取事件會被 trigger。
儘管大部分的事件都屬於冒泡事件,但是我們可以自定義事件為擷取事件,這樣在 top-down 的過程就可以處理需要觸發的事件了。
這邊來介紹一個 React 提供但是很多人不知道的東西,叫做:onClickCapture。
onClick 是在冒泡階段會 trigger 的事件,但是如果你想要在擷取階段 trigger 的話,可以使用:onClickCapture

實作例子

如果你要製作 Card,點擊卡片會開啟新視窗,但是上面又有其他按鈕時,這個時候你就需要針對事件去做處理。

1
2
3
4
5
6
7
8
9
10
<Card key={card.id} onClick={open("/card", "_blank")}>
<div
onClick={(event) => {
event.stopPropagation();
putCollection(card.id);
}}
>
// ...
</div>
</Card>

關於 React 的冒泡與擷取事件

因為原生的事件處理有少部分是屬於擷取事件,例如:onfocus, onblur, onchange……
React 為了單一化,讓所有的事件都變成了冒泡事件
像是以下的專案在 React 上執行,你會發現執行之後 onfocus 變成了冒泡事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import React from "react";

function App() {
return (
<div
style={{ background: "yellow", padding: "50px" }}
onFocus={() => console.log("focused yellow")}
onFocusCapture={() => console.log("captured yellow")}
>
<div
style={{ background: "blue", padding: "50px" }}
onFocus={() => console.log("focused blue")}
onFocusCapture={() => console.log("captured blue")}
>
<div
style={{ background: "green", padding: "50px" }}
onFocus={() => console.log("focused green")}
onFocusCapture={() => console.log("captured green")}
>
<input
onFocus={() => console.log("focused input")}
onFocusCapture={() => console.log("captured input")}
/>
</div>
</div>
</div>
);
}

export default App;

再來一個範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import React from "react";

function App() {
return (
<div
style={{ background: "yellow", padding: "50px" }}
onChange={() => console.log("changed yellow")}
onChangeCapture={() => console.log("captured yellow")}
>
<div
style={{ background: "blue", padding: "50px" }}
onChange={() => console.log("changed blue")}
onChangeCapture={() => console.log("captured blue")}
>
<div
style={{ background: "green", padding: "50px" }}
onChange={() => console.log("changed green")}
onChangeCapture={() => console.log("captured green")}
>
<input
onChange={() => console.log("changed input")}
onChangeCapture={() => console.log("captured input")}
/>
</div>
</div>
</div>
);
}

export default App;

在 React17 之後,有一個比較大的更動
17 之前會在 document 運行 document.addEventListener()
17 之後會在 root 運行 rootNode.addEventListener()
(從 debug tools 選取 root 再查看 event litener 就可以看出)

補充資料

  1. https://javascript.info/bubbling-and-capturing
  2. https://www.google.com/url?sa=t&rct=j&q=&esrc=s&source=web&cd=&cad=rja&uact=8&ved=2ahUKEwjw9f6w7InyAhXJGaYKHRXRBicQFjABegQIBRAD&url=https%3A%2F%2Fcodesandbox.io%2Fs%2Fju5fz&usg=AOvVaw0dwXidXILLy-e42TMYKUFu
  3. https://betterprogramming.pub/whats-the-difference-between-synthetic-react-events-and-javascript-events-ba7dbc742294