当点击页面的某个按钮时,单击事件并不仅仅发生在按钮上。换句话说,在单机按钮的同时,也点击了按钮的容器元素,甚至也单机了整个页面。
==事件流描述的是从页面中接受时间的顺序==,IE和Netscape提出了几乎相反的事件流的概念:
- IE的事件流是事件冒泡流
- Netscape是事件捕获流
==大多是情况下都是将事件处理程序添加到事件冒泡阶段==
事件冒泡
IE的事件流叫做事件冒泡(event bubbling),即事件开始时由最具体的元素(嵌套层次最深的节点)接受,然后逐级向上传播,直至文档根节点。
事件冒泡是自内而外的去触发各个层次绑定的事件
以下面的HTML页面为例:
<!DOCTYPE html>
<html>
<head>
<title>Event Bubling Example</title>
</head>
<body>
<div id="myDiv">Click Me</div>
</body>
</html>
当点击了<div>
元素,click
事件会按照如下顺序传播
graph LR
div-->body
body-->html
html-->document
事件从我们单击的元素开始,沿着DOM树向上传播,在每一级节点上都会发生,直至传播到document
对象
事件捕获
Netscape提出的另一种事件流是事件捕获(event capturing),即不太具体的节点应该更早接收到事件,最具体的节点应该最后接收到事件。事件捕获的用于在于事件到达预订目标之前捕获它。
仍然以上面的页面作为例子,在事件捕获的状态下,点击<div>
元素,click
事件会按照相反的顺序传播:
graph LR
document-->html
html-->body
body-->div
事件捕获过程中,document
对象最先接收到click
事件,然后事件沿DOM树向下,直至传播到事件的实际目标,即<div>
元素
DOM事件流
==DOM2级事件规定的事件流包括了三个阶段==:
- 事件捕获阶段
- 处于目标阶段
- 事件冒泡阶段
首先发生的事件捕获,为截获事件提供了机会。然后是实际的目标接收到事件,最后一个阶段是冒泡阶段,可以在这个阶段对事件作出相应。
以前面的HTML页面为例,单机<div>
元素会按照下图顺序触发事件
在DOM事件流中,实际的目标<div>
元素在捕获阶段不会接收到事件,这意味着在捕获阶段事件从document
到html
再到body
后就停止了。下一个阶段是“处于目标”阶段,事件在<div>
上发生。然后冒泡阶段发生,事件有传播回document
==在事件处理中,事件处于目标阶段被看成冒泡阶段的一部分。==
==为了最大限度的兼容,大多是情况下都是将事件处理程序添加到事件冒泡阶段。不是特别需要,不建议在事件捕获阶段注册事件处理程序==
DOM2级事件处理程序
DOM2级事件定义了对两个方法:
-
addEventListener
:添加事件处理程序 -
removeEventListener
:删除事件处理程序
所有DOM节点都包括这两个方法,它们都接受3个参数,前两个参数是要处理的事件名和作为事件处理程序的函数。
第三个参数是一个布尔值,如果是true
,表示在捕获阶段调用事件处理程序,如果是false
,表示在冒泡阶段调用事件处理程序。默认为false
看一个例子:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>事件冒泡</title>
<style>
#outer {
width: 500px;
height: 300px;
background: darkcyan;
}
#inner {
width: 300px;
height: 150px;
background: chocolate;
}
</style>
</head>
<body>
<div id="outer">Parent
<div id="inner">Child</div>
</div>
</body>
<script>
const outer = document.getElementById('outer'),
inner = document.getElementById('inner');
outer.addEventListener('click', function () {
console.log('parent')
});
inner.addEventListener('click', function () {
console.log('child')
});
</script>
</html>
而点击child
,事件在冒泡阶段触发,首先被触发的是child
本身,打印了child
,然后向上层冒泡,触发parent
的事件,打印出parent
将addEventListener
的第三个参数改变为true
,则事件触发由冒泡阶段转为捕获阶段,此时,当点击child
,事件在捕获阶段的传播从document
开始,向下传播到parent
,然后再到child
本身,所以会先打印parent
,然后才打印出child
当冒泡阶段和捕获阶段都会调用事件处理程序时,事件按DOM事件流的顺序执行时间处理程序
当事件处于目标阶段时,事件调用顺序决定于绑定事件的书写顺序。
outer.addEventListener('click', function() {
console.log('parent冒泡')
}, false);
outer.addEventListener('click', function() {
console.log('parent捕获')
}, true);
inner.addEventListener('click', function() {
console.log('child冒泡')
}, false);
inner.addEventListener('click', function() {
console.log('child捕获')
}, true);
上面的代码,由于child是处于目标阶段的事件,执行的顺序和我们书写的顺序有关,因此是'child冒泡'
在前,而'child捕获'
在后。而parent
的事件就会按照DOM事件流顺序执行,即先'parent捕获'
后'parent冒泡'
因此,这种情况下的调用顺序是:
graph LR
parent捕获-->child冒泡
child冒泡-->child捕获
child捕获-->parent冒泡
如果我们改动一下书写顺序:
outer.addEventListener('click', function() {
console.log('parent冒泡')
}, false);
outer.addEventListener('click', function() {
console.log('parent捕获')
}, true);
inner.addEventListener('click', function() {
console.log('child捕获')
}, true);
inner.addEventListener('click', function() {
console.log('child冒泡')
}, false);
那么调用顺序就变成了:
graph LR
parent捕获-->child捕获
child捕获-->child冒泡
child冒泡-->parent冒泡
事件委托
事件委托就是把一个元素响应事件的函数委托到父层或者更外层元素上,真正绑定事件的是外层元素,当事件响应到需要绑定的元素上时,会通过事件冒泡机制从而触发它的外层元素的绑定事件上,然后在外层元素上去执行函数。
事件委托的优点:
- 提高性能
- 动态绑定事件(不必再为动态生成的、需要绑定事件的元素逐一绑定事件)
这样的一个例子:鼠标放到li上
,让对应的li
背景变红:
<body>
<ul id="ul">
<li>item1</li>
<li>item2</li>
<li>item3</li>
</ul>
</body>
<script>
document.querySelector('#ul').addEventListener('mouseover', function (e) {
if(e.target.tagName.toLowerCase() === 'li') {
document.querySelectorAll('#ul li').forEach(v => {
if(v.tagName.toLowerCase() === 'li') {
v.style.backgroundColor = '#fff'
}
});
e.target.style.backgroundColor = 'red'
}
});
// 上面的forEach遍历也可以通过再添加一个事件处理程序完成
// document.querySelector('#ul').addEventListener('mouseout', function (e) {
// if(e.target.tagName.toLowerCase() === 'li') {
// e.target.style.backgroundColor = '#fff'
// }
// })
</script>
</html>
target
属性可返回事件的目标节点(触发该事件的节点),如生成事件的元素、文档或窗口。
这样避免了在所有的li
上绑定事件,而且如果是动态生成的节点,直接在li
上绑定事件就需要再次绑定,而事件委托就不需要。
利用jquery也可以实现:
$("ul").on("click","li",function(){
// some code here
})
其中on
方法的第二个参数是一个jQuery选择器,用于指定哪些后代元素可以触发绑定的事件。如果该参数为null
或被省略,则表示当前元素自身绑定事件(实际触发者也可能是后代元素,只要事件流能到达当前元素即可)。
阻止事件冒泡
event.stopPropagation()
可以使用event.stopPropagation()
来阻止事件继续传播,在上面的parent
和child
的例子中,如果将child
绑定的事件改为下面的代码:
outer.addEventListener('click', function() {
console.log('parent冒泡')
}, false);
inner.addEventListener('click', function() {
console.log('child冒泡');
e.stopPropagation()
}, false);
这样再次点击child
,就不会触发parent
的click
事件
e.target
也可以通过e.target
来阻止事件冒泡
document.body.addEventListener('click', e = > {
// 通过e.target判断阻止冒泡
if (e.target && e.target.matches('a')) {
return;
}
})
return false
JS的原生的事件绑定中,return false
是不能阻止事件冒泡的,它可以用来阻止浏览器的默认事件,作用于event.preventDefault()
相同
在jQuery绑定的事件中使用return false
,既阻止默认行为又停止冒泡
参考
- 《JavaScript高级程序设计》第十三章
- 掘金 - JavaScript事件流
- segmentfault - JS中事件冒泡与捕获
- 知乎 - JavaScript 事件委托详解