什么是事件流:事件流描述的是从页面中接收事件的顺序,DOM 2级事件流包括下面几个阶段。
(1) 事件捕获阶段
(2) 处于目标阶段
(3) 事件冒泡阶段
如何让事件先冒泡后捕获:
在 DOM 标准事件模型中,是先捕获后冒泡。但是如果要实现先冒泡后捕获的效果,对于同一个事件,监听捕获和冒泡,分别对应相应的处理函数,监听到捕获事件,先暂缓执行,直到冒泡事件被捕获后再执行捕获之间。
一、addEventListener
addEventListener 方法将指定的监听器注册到 EventTarget 上,当该对象触发指定的事件时,指定的回调函数就会被执行。
addEventListener 事件目标可以是文档上的元素 Element、Document 和 Window 或者任何其他支持事件的对象(例如 XMLHttpRequest)。
参考文档:https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget/addEventListener
语法:
target.addEventListener(type, listener, options/useCapture)
-
type:表示监听事件类型的字符串
-
listener:所监听的事件触发,会接受一个事件通知对象。
-
options:一个指定有关 listener 属性的可选参数对象。可选值有 capture(事件捕获阶段传播到这里触发)、once(在 listener 添加之后最多值调用一次)、passive(设置为 true 时表示listener 永远不会调用 preventDefault())。
-
useCapture:在 DOM 树中,注册了 listener 的元素,是否要先于它下面的 EventTarget 调用该 listener。
注:addEventListener 的第三个参数涉及到冒泡和捕获,为 true 时是捕获,为 false 时是冒泡。
或者是一个对象 { passive: true },针对的是 Safari 浏览器,禁止/开启使用滚动的时候要用到
示例:
<table id="outside">
<tr>
<td id="t1">one</td>
</tr>
<tr>
<td id="t2">two</td>
</tr>
</table>
<script>
// 这个示例简单实现了点击 two 切换到 four,点击 four 再切换到 two 的效果。
(function () {
// 添加函数
const modifytext = (text) => {
const t2 = document.querySelector('#t2');
if (t2.firstChild.nodeValue === text) {
t2.firstChild.nodeValue = 'two';
} else {
t2.firstChild.nodeValue = text;
}
}
// 给table添加事件监听器
const element = document.querySelector('#outside');
element.addEventListener('click', function () {
modifytext('four')
}, false)
})()
</script>
二、原理
事件捕获和事件冒泡分别是 网景(Netscape)和 IE 对 DOM 事件产生顺序的描述。
网景 认为 DOM 接收的事件应该最先是 window,然后到 document,接着一层一层往下,最后才到具体的元素接收到事件,即 事件捕获。
IE 则认为 DOM 事件应该是具体元素先接收到,然后再一层一层往上,接着到 document,最后才到 window,即 事件冒泡。
最后 W3C 对这两种方案进行了统一:将 DOM 事件分为两个阶段,事件捕获和事件冒泡阶段。
当一个元素被点击,首先是事件捕获阶段,window 最先接收事件,然后一层一层往下捕获,最后由具体元素接收;之后再由具体元素再一层一层往上冒泡,到 window 接收事件。
所以:
事件冒泡:当给某个目标元素绑定了事件之后,这个事件会依次在它的父级元素中被触发(当然前提是这个父级元素也有这个同名称的事件,比如子元素和父元素都绑定了 click 事件就触发父元素的 click)。
事件捕获:和冒泡相反,会从上层传递到下层。
三、案例
<ul class="ul">
<li class="li">
<button class="btn">点我</button>
</li>
</ul>
<script>
window.onload = function () {
const btn = document.querySelector('.btn');
btn.addEventListener('click', function () {
console.log('button');
})
const li = document.querySelector('.li');
li.addEventListener('click', function () {
console.log('li');
})
const ul = document.querySelector('.ul');
ul.addEventListener('click', function () {
console.log('ul');
})
document.addEventListener('click', () => {
console.log('document');
})
window.addEventListener('click', () => {
console.log('window');
})
}
</script>
Chrome 输出下顺序是:button -> li -> ul -> document -> window
如果是捕获的话,那么则相反
四、练习
点击一个 input 依次触发的事件
<input type="text" id="text">
<script>
const text = document.getElementById('text');
text.onclick = function (e) {
console.log('onclick');
}
text.onfocus = function (e) {
console.log('onfocus');
}
text.onmousedown = function (e) {
console.log('onmousedown');
}
text.onmouseenter = function (e) {
console.log('ommouseenter');
}
// text.onmouseup = function (e) {
// console.log('onmouseup');
// }
</script>
正确顺序是:onmouseenter -> onmousedown -> onfocus -> onclick。
如果加上 onmouseup,那就是:
onmouseenter -> onmousedown -> onfocus -> onmouseup -> onclick
五、阻止冒泡
event.stopPropagation();
<script>
btn.addEventListener('click', function () {
console.log('button');
event.stopPropagation();
})
</script>
通过阻止冒泡,程序只会输出 button,而不会继续输出 li 等。
六、onmouseover 和 onmouseenter 区别
这两者都是移入的时候触发,但是 onmouseover 会触发多次,而 onmouseenter 只在进去的时候才触发。
七、科普
并不是所有的事件都有冒泡,例如:
onblur
onfocus
onmouseenter
onmouseleave