本篇内容参考连结
TechBridge 技术共笔部落格
Capture and Bubble
Definition
假如网页做久了, 一定会用到下面几个关键字的函式 addEventListener
, preventDefault
和 stopPropagation
. 而这两个(缺少哪一个之后会揭晓)使用的方式将会跟今天的主题有很大的关係.简单来说, 就是要讲述事件触发后, 在各个dom物件传递的顺序.
Event Phase
这个机制主要分为三个部分, 依照顺序分别为
Capturing Phase : 捕捉阶段, 为事件触发时的初始阶段At Target Phase : 目标阶段, 为事件触发的终点Bubbling Phase : 冒泡阶段, 为事件的最后阶段可以先参考以下的图来理解
首先要知道整个HTML的架构会像是一个树状图, 然后当你点击了<table>
里面的其中一个<td>
, 事件并不是从<td>
开始触发和传递, 而是从最上层Window开始一步一步传下来直到目标, 而这个阶段就是CAPTURING_PHASE. 当事件到达目标<td>
后, 会进入下一个阶段AT_TARGET. 然后再慢慢地往回飘上去(因为是Bubble), 这个阶段就是BUBBLING_PHASE. (口诀 : 先捕获, 在冒泡)
addEventListener
而JS里面聆听事件, 并做callback的函式, 就是addEventListener
. 其实他还有第三个参数, true
代表添加这个callback到捕获阶段, 而false
则是加到冒泡阶段.
借用上面这一张图, 写了一个测试範例
eventPhase : 事件的阶段
1: Capturing
2: At Target
3: Bubbleing
<!DOCTYPE html><html> <body> <ul id="ul_item"> <li id="li_item"> <a id="a_link">trigger Here</a> </li> </ul> <script> const ul_item = document.getElementById("ul_item"); const li_item = document.getElementById("li_item"); const a_link = document.getElementById("a_link"); // ul 的捕获 ul_item.addEventListener('click', (e) => { console.log('ul capturing', e.eventPhase); }, true) // ul 的冒泡 ul_item.addEventListener('click', (e) => { console.log('ul bubbling', e.eventPhase); }, false) // li 的捕获 li_item.addEventListener('click', (e) => { console.log('li capturing', e.eventPhase); }, true) // li 的冒泡 li_item.addEventListener('click', (e) => { console.log('li bubbling', e.eventPhase); }, false) // a 的捕获 a_link.addEventListener('click', (e) => { console.log('a capturing', e.eventPhase); }, true) // a 的冒泡 a_link.addEventListener('click', (e) => { console.log('a bubbling', e.eventPhase); }, false) </script> </body></html>
当点击#a_link时, 结果得到 :
从範例中就可以看到实际运行状况, 不管顺序如何改变, 都会符合先捕捉后冒泡的顺序.
Target Phase
再参考文件中有提到另一个测试, 当在Target Element, 把Capture listener和Bubble listener的编写顺序颠倒, 会得到先输出 Bubble listener, 这是因为都在Target Phase, 所有就没有捕捉或冒泡的关係, 从而根据加入Macrotask的顺序来做输出. 然后也是根据不同的浏览器会有不一样的输出.
在新版的 Chrome 似乎更改了这个行为,请参考:Chrome 89 更新事件触发顺序,导致99%的文章都错了(包括MDN)
在Target Phase, 也会依照先捕捉后冒泡的顺序来输出.
stopPropagation vs stopImmediatePropagation
stopPropagation()
从字面上的意思就可以知道事件到了这个listener就会停止传递到下一个节点, 但在同一个element不同listener还是会被触发. 若想让同一层级的listner不被执行,则可改用stopImmediatePropagation()
;
preventDefault
这个函式则仅是取消Dom的预设行为 (例如: 像是我们点击超连结<a>
,他的预设行为是开新分页或跳转, 当listener添加了 preventDefault()
后, 则就会取消此行为), 并不会影响事件的传递, 但会影响之后节点的预设行为, 并一併取消(例如: 当在母节点中, 添加preventDefault()
后, 等事件传递到之后的超连结, 也都会失效).所以上面三个函式中, 就是他与事件传递机制无关.