---
# System prepended metadata

title: '[JavaScript] DOM 事件的傳遞：先捕獲，再冒泡'
tags: [前端筆記]

---

# [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）。
![](https://i.imgur.com/ZLhat4M.png)
- 點到 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 出
![](https://i.imgur.com/Dtrertz.png)
因為先捕獲（從根節點一層一層到目標）
抵達目標
再從目標冒泡回去根節點

為了確認這個例子，將代碼的順序顛倒過來（變成冒泡在前面），如果真的是先捕獲再冒泡，那麼結果應該不會有所不同。

```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);

```
得到相同的結果！（即使寫的順序不同，但有下什麼時候執行的參數，所以要符合階段才會執行）
![](https://i.imgur.com/ffQeltP.png)
*註：當事件傳到目標本身，沒有分 捕獲 冒泡 之分，一切按照代碼順序！*

>這也就是為什麼這個口號的由來 => 先捕獲，再冒泡

### 那要如何取消事件的傳遞鏈呢 ？`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 捕獲階段而已
![](https://i.imgur.com/TIFphlf.png)

但是 `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` 停止繼續往上層冒泡，所以容器的冒泡時期事件不會觸發。
![](https://i.imgur.com/LJrbqkV.png)

如果把項目 `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);
```

![](https://i.imgur.com/e2ecSwi.png)

### 與 `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



