JS 事件冒泡和事件捕获

写在前面

W3C规定DOM事件流(event flow )存在三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段。dom标准事件流的触发的先后顺序为:先捕获再冒泡,即当触发dom事件时,会先进行事件捕获,捕获到事件源之后通过事件传播进行事件冒泡。

JS 事件冒泡和事件捕获

对事件冒泡和捕捉的解释

事件冒泡

在本示例中,当我们点击孙子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('孙子')
})

JS 事件冒泡和事件捕获

事件冒泡demo在线预览

事件捕获

事件捕获刚好和事件冒泡相反,事件触发时是先检查最外层祖先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) // 注意:这个地方多了一个参数

JS 事件冒泡和事件捕获

事件捕获demo在线预览

理解DOM事件的例子

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');
})

关闭浮层demo在线预览

事件委托

使用事件委托实现点击点击不同的 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)
  }
})

事件委托demo

e.target 和 e.currentTarget 的区别

e.target 是当前触发事件的元素

e.currentTarget 是事件绑定的元素

JS 事件冒泡和事件捕获

参考

常见的事件列表

事件介绍

EventListener

Event Flow

编写一个EventUtil工具类实现事件管理兼容

上一篇:addEventListener绑定传参函数并解绑


下一篇:梳理ajax