JS 事件冒泡和事件捕获
写在前面
W3C规定DOM事件流(event flow )存在三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段。dom标准事件流的触发的先后顺序为:先捕获再冒泡,即当触发dom事件时,会先进行事件捕获,捕获到事件源之后通过事件传播进行事件冒泡。
对事件冒泡和捕捉的解释
事件冒泡
在本示例中,当我们点击孙子div
的时候浏览器就会去检查这个div身上没有绑定事件,如果有就执行它;然后再检查它的父元素(儿子div
)身上有没有绑定事件,如果有就执行;然后再检查儿子div
的父元素身上有没有绑定事件,如果有就执行,依此类推,直到html
根元素,这样由内到外的向外触发事件就叫做事件冒泡。
let parent = document.querySelector('.parent')
let child = document.querySelector('.child')
let grandson = document.querySelector('.grandson')
parent.addEventListener('click', function(e){
console.log('爸爸')
})
child.addEventListener('click', function(e){
console.log('儿子')
})
grandson.addEventListener('click', function(e){
console.log('孙子')
})
事件捕获
事件捕获刚好和事件冒泡相反,事件触发时是先检查最外层祖先html
元素没有绑定事件,如果有则执行它,然后在检查,然后,它移动到<html>
中单击元素的下一个祖先元素,并执行相同的操作,然后是单击元素再下一个祖先元素,依此类推,直到到达实际点击的元素,就和剥洋葱一样。
let parent = document.querySelector('.parent')
let child = document.querySelector('.child')
let grandson = document.querySelector('.grandson')
parent.addEventListener('click', function(e){
console.log('爸爸')
}, true) // 注意:这个地方多了一个参数
child.addEventListener('click', function(e){
console.log('儿子')
}, true) // 注意:这个地方多了一个参数
grandson.addEventListener('click', function(e){
console.log('孙子')
}, true) // 注意:这个地方多了一个参数
addEventListener
在开发中,我们都是通过addEventListener
给元素添加事件处理代码,可以同时给同一个元素添加多个事件处理程序代码
grandson.addEventListener('click', function(e){
console.log('孙子')
})
grandson.addEventListener('click', function(e){
console.log('孙子')
})
// 控制台会打印2次孙子
默认情况下,所有事件处理程序都是在冒泡阶段注册的,如果想在捕获阶段注册一个事件,那么你可以通过使用addEventListener()
注册您的处理程序,并将可选的第三个参数useCapture
设置为true,capture
就是捕获的意思
grandson.addEventListener('click', function(e){
console.log('孙子')
}, true) // 第三个参数 useCapture 设置为 true
阻止默认行为
e.preventDefault()
阻止事件传播
e.stopPropagation()
实战
点击别处关闭浮层Demo
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<div class="container">
<button id="btnOpen">打开浮动层</button>
<div class="popover" id="popoverlayer">
这里是浮动层中显示的内容
<br>
<label ><input type="checkbox" />一个checkbox</label>
</div>
</div>
</body>
</html>
body{
border: 5px solid red
}
.container{
border: 1px solid blue;
position: relative;
display: inline-block
}
.popover{
display: none;
position: absolute;
top: 0;
left: 100%;
width: 200px;
height: 200px;
margin-left: 10px;
background: #fff;
border: 1px solid #000;
}
.popover::before{
content: '';
position: absolute;
top: 5px;
left: -20px;
border: 10px solid transparent;
border-right: 10px solid #000;
}
.popover::after{
content: '';
position: absolute;
top: 5px;
left: -19px;
border: 10px solid transparent;
border-right: 10px solid #fff;
}
.popover.active{
display: block;
}
let container = document.querySelector('.container');
let btnOpen = document.querySelector('#btnOpen');
let popoverlayer = document.querySelector('#popoverlayer');
btnOpen.addEventListener('click', function(e){
console.log('click');
popoverlayer.classList.add('active');
e.stopPropagation();
// 方法2:注册 document click 只执行一次
// document.addEventListener('click', function(){
// console.log('document once click');
// popoverlayer.classList.remove('active');
// }, {once: true})
})
popoverlayer.addEventListener('click', function(e){
console.log('layer click');
// 阻止传播,为了避免点击浮层时不关闭
e.stopPropagation();
})
// 注意这里监听的是 document,而不是body
// 因为当body的高度不够的时候,点击浮层的外部是不会关闭的
// 方法1
document.addEventListener('click', function(){
console.log('document click');
popoverlayer.classList.remove('active');
})
事件委托
使用事件委托实现点击点击不同的 li 元素
来获取歌曲的 id
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bin</title>
</head>
<body>
<ul class="musiclist">
<li data-id="1"><span>歌曲1</span><span>作者1</span></li>
<li data-id="2"><span>歌曲2</span><span>作者2</span></li>
<li data-id="3"><span>歌曲3</span><span>作者3</span></li>
<li data-id="4"><span>歌曲4</span><span>作者4</span></li>
<li data-id="5"><span>歌曲5</span><span>作者5</span></li>
</ul>
</body>
</html>
*{
padding: 0;
margin:0;
box-sizing: border-box;
}
body{
border: 1px solid #000;
padding: 10px;
}
ul,ol{
list-style: none;
}
.musiclist{
}
.musiclist li{
display: block;
padding: 15px 10px;
background: #fff;
border-bottom: 1px solid #ddd;
}
.musiclist li:hover{
background: #eee;
}
.musiclist li>span:first-child{
display: inline-block;
font-size: 16px;
color: #000;
}
.musiclist li>span:nth-child(2){
font-size: 10px;
color: #000;
margin-left: 10px;
}
let musiclist = document.querySelector('.musiclist');
// 事件处理程序绑定在歌曲列表上面
musiclist.addEventListener('click', function(e){
console.log('target', e.target);
console.log('currentTarget', e.currentTarget);
let target = e.target;
// 当点击的元素是 li 时,才能获取到 dataset 属性
if(target.nodeName === 'LI'){
console.log('当前歌曲ID是' + target.dataset.id)
}
})
e.target 和 e.currentTarget 的区别
e.target 是当前触发事件的元素
e.currentTarget 是事件绑定的元素