# [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