# [JavaScript] DOM 事件的傳遞:先捕獲,再冒泡
###### tags: `前端筆記`
## `DOM` 傳遞事件的相關代碼
1. `element.addEventListener` => 為一元素新增事件
2. `event.stopPropagation` => 阻擋事件冒泡傳遞
3. `event.preventDefault` => 停止元素的預設行為
## 什麼是 先捕獲,再冒泡?
### `DOM` 傳遞事件的階段
`DOM` 的事件在傳播的時候其實會按照順序分成三個階段:
1. Capture Phase => 捕獲階段
2. Target Phase => 到目標了
3. Bubbling Phase => 冒泡階段
在 `addEventListener` 還可以下第三個參數,表示該事件是在哪個時候觸發。
- `true` => 在 捕獲階段
- `false` 或者 不輸入 => 在冒泡階段
- 用 `event.eventPhase` 可以看到回傳的數字,代表該事件在什麼階段執行。
- 1 = CAPTURING_PHASE, 2 = AT_TARGET, 3 = BUBBLING_PHASE
### 那是如何被傳遞的呢?
`DOM` 傳遞事件的階段順序 Capture Phase(捕獲)=> Target Phase(目標)=> Bubbling Phase(冒泡)
1. 捕獲階段
會從根節點(window)出發,慢慢地從最外層的元素往目標靠近。
2. 目標階段
事件抵達到了目標,執行第三階段。
3. 冒泡階段
從目標開始慢慢地一層一層回去到根節點(window)。

- 點到 td => 觸發事件
- 開始事件傳遞
- 先從根節點(window)再依照結構一層一層送事件到目標,也就是 `<td>`
- 抵達 `<td>` 後就慢慢一層一層回去到根節點
以下為實際範例:
```HTML=
<!DOCTYPE html>
<html>
<body>
<ul id="list">
<li id="list_item">
<a id="list_item_link" target="_blank" href="http://google.com">
google.com
</a>
</li>
</ul>
<script src="./app.js"></script>
</body>
</html>
```
```javascript=
const get = (id) => document.getElementById(id);
const $list = get('list');
const $listItem = get('list_item');
const $listItemLink = get('list_item_link');
// list 的捕獲
$list.addEventListener('click', (event) => {
console.log('list capturing', event.eventPhase);
event.preventDefault();
}, true);
// list 的冒泡
$list.addEventListener('click', (event) => {
console.log('list bubbling', event.eventPhase);
}, false);
// list_item 的捕獲
$listItem.addEventListener('click', (event) => {
console.log('list_item capturing', event.eventPhase);
console.log(this);
}, true);
// list_item 的冒泡
$listItem.addEventListener('click', (event) => {
console.log('list_item bubbling', event.eventPhase);
console.log(this.tagName);
}, false);
// list_item_link 的捕獲
$listItemLink.addEventListener('click', (event) => {
console.log('list_item_link capturing', event.eventPhase);
}, true);
// list_item_link 的冒泡
$listItemLink.addEventListener('click', (event) => {
console.log('list_item_link bubbling', event.eventPhase);
}, false);
```
此範例點擊連結時會 console.log 出

因為先捕獲(從根節點一層一層到目標)
抵達目標
再從目標冒泡回去根節點
為了確認這個例子,將代碼的順序顛倒過來(變成冒泡在前面),如果真的是先捕獲再冒泡,那麼結果應該不會有所不同。
```javascript=
const get = (id) => document.getElementById(id);
const $list = get('list');
const $listItem = get('list_item');
const $listItemLink = get('list_item_link');
// list 的冒泡
$list.addEventListener('click', (event) => {
console.log('list bubbling', event.eventPhase);
}, false);
// list 的捕獲
$list.addEventListener('click', (event) => {
console.log('list capturing', event.eventPhase);
event.preventDefault();
}, true);
// list_item 的冒泡
$listItem.addEventListener('click', (event) => {
console.log('list_item bubbling', event.eventPhase);
}, false);
// list_item 的捕獲
$listItem.addEventListener('click', (event) => {
console.log('list_item capturing', event.eventPhase);
}, true);
// list_item_link 的冒泡
$listItemLink.addEventListener('click', (event) => {
console.log('list_item_link bubbling', event.eventPhase);
}, false);
// list_item_link 的捕獲
$listItemLink.addEventListener('click', (event) => {
console.log('list_item_link capturing', event.eventPhase);
}, true);
```
得到相同的結果!(即使寫的順序不同,但有下什麼時候執行的參數,所以要符合階段才會執行)

*註:當事件傳到目標本身,沒有分 捕獲 冒泡 之分,一切按照代碼順序!*
>這也就是為什麼這個口號的由來 => 先捕獲,再冒泡
### 那要如何取消事件的傳遞鏈呢 ?`event.stoppropagation`
Q: 有時候不想要觸發多次的事件,要怎麼中途中斷事件傳送鏈呢?
A: `event.stopPropagation` 可以中斷事件的傳遞,加在哪邊,就會阻斷那邊之後的傳遞。
加在 外層 list 的捕獲階段,這樣子事件只會傳到 list 的捕獲階段變停止。
```javascript=
const get = (id) => document.getElementById(id);
const $list = get('list');
const $listItem = get('list_item');
const $listItemLink = get('list_item_link');
// list 的冒泡
$list.addEventListener('click', (event) => {
console.log('list bubbling', event.eventPhase);
}, false);
// list 的捕獲
$list.addEventListener('click', (event) => {
console.log('list capturing', event.eventPhase);
event.preventDefault();
event.stopPropagation();
}, true);
// list_item 的冒泡
$listItem.addEventListener('click', (event) => {
console.log('list_item bubbling', event.eventPhase);
}, false);
// list_item 的捕獲
$listItem.addEventListener('click', (event) => {
console.log('list_item capturing', event.eventPhase);
}, true);
// list_item_link 的冒泡
$listItemLink.addEventListener('click', (event) => {
console.log('list_item_link bubbling', event.eventPhase);
}, false);
// list_item_link 的捕獲
$listItemLink.addEventListener('click', (event) => {
console.log('list_item_link capturing', event.eventPhase);
}, true);
```
只有在 list 捕獲階段而已

但是 `event.stoppropagation` 顧名思義只是「阻擋冒泡」而已,所以如果容器有「捕獲」時期的事件還是會依照事件傳遞的順序「先捕獲後冒泡」,觸發容器的捕獲時期事件。
```htmlembedded=
<div class="container">
<h1>Hello</h1>
</div>
```
```javascript=
const containerDiv = document.querySelector('.container');
const h1 = document.querySelector('h1');
// 容器「捕獲」時期的事件
containerDiv.addEventListener('click', () => console.log('div capture'), true);
// 項目「捕獲」時期的事件
h1.addEventListener('click', () =>{
console.log('h1 capture')
}, true);
// 容器「冒泡」時期的事件
containerDiv.addEventListener('click', () => console.log('div'), false);
// 項目「冒泡」時期的事件
h1.addEventListener('click', (event) => {
console.log('h1')
event.stopPropagation();
}, false);
```
點擊項目 `h1`,待 `h1` 執行冒泡時期的事件變因 `stopPropagation` 停止繼續往上層冒泡,所以容器的冒泡時期事件不會觸發。

如果把項目 `h1` 的捕獲時期事件寫入 `stopPropagation` 的話就會停止事件傳遞,==事件到該節點完變停止==。
```javascript=
const containerDiv = document.querySelector('.container');
const h1 = document.querySelector('h1');
// 容器「捕獲」時期的事件
containerDiv.addEventListener('click', () => console.log('div capture'), true);
// 項目「捕獲」時期的事件
// stopPropagation 在事件傳遞到這個節點變停止傳遞
h1.addEventListener('click', () =>{
console.log('h1 capture');
event.stopPropagation();
}, true);
// 容器「冒泡」時期的事件
containerDiv.addEventListener('click', () => console.log('div'), false);
// 項目「冒泡」時期的事件
h1.addEventListener('click', (event) => console.log('h1'), false);
```

### 與 `DOM` 事件傳遞毫無關係的 `event.preventDefault`
`event.preventDefault` 是用來停止元素的預設行為(點擊 a 元素不會跳到指定網址、點擊 submit 不會跳轉網頁 form 等等...)跟事件的傳遞沒關係。
但如果在外層使用 `event.preventDefault`,藉由 `DOM` 事件的傳送,內層的元素也會吃到同樣的效果。
```HTML=
<!DOCTYPE html>
<html>
<body>
<ul id="list">
<li id="list_item">
<a id="list_item_link" target="_blank" href="http://google.com">
google.com
</a>
</li>
</ul>
<script src="./app.js"></script>
</body>
</html>
```
外層 list 有 `event.preventDefault`,點擊內層 a 元素也不會到指定的頁面。
```javascript=
const get = (id) => document.getElementById(id);
const $list = get('list');
const $listItem = get('list_item');
const $listItemLink = get('list_item_link');
// list 的捕獲
$list.addEventListener('click', (event) => {
console.log('list capturing', event.eventPhase);
event.preventDefault();
}, true);
```
## 實際開發的幫助
*008天重新認識 JavaScript => P.4-34 ~ 4-46*
點擊任一元素,其實不僅僅代表只有點到該元素,其實也是包含了點擊該元素 + 該元素上層的所有元素。
因此如果今天有一個 `ul` 裡面有 1000 個 `li`,然而每個 `li` 都要綁定事件的話,就只要綁定 `li` 上層的元素 `ul`,透過 `DOM` 事件的傳遞觸發 `ul` 的事件。
點擊 `li` = `li` 本身 + 上層全部的元素都被點到了!那只要確保我點的是 `li` 就好啦 XD!
```javascript=
// getElementsByTagName 才會更新 Nodelist
const ul = document.getElementsByTagName('ul');
ul.addEventListener('click' (event) => {
if (event.target.tagName.toLowerCase() === 'li') {
console.log(event.target.textContent);
}
});
```
## 參考資料
1. [DOM 的事件傳遞機制:捕獲與冒泡](https://blog.huli.tw/2017/08/27/dom-event-capture-and-propagation/)
2. [DOM 事件的圖片詳解 w3c](https://www.w3.org/TR/DOM-Level-3-Events/#event-flow)
### 書籍
1. 008 天重新認識 JavaScript