继续学习DOM事件模型
继续深入一下对于DOM事件模型的学习,理解事件捕获与冒泡。
假设我们有一个需求,点击之后浮层出现;点击别处浮层消失。
<!DOCTYPE html>
<html>
<head>
<script src="//code.jquery.com/jquery-1.9.1.min.js"></script>
<meta charset="utf-8">
<title>JS Bin</title>
</head>
<body>
<div class='wrapper' id='wrapper'>
<button id='clickMe'>点击</button>
<div class="popover" id='popover'>浮层</div>
</div>
</body>
</html>
.wrapper {
position: relative;
display: inline-block;
}
.popover {
border:1px solid red;
position: absolute;
left:100%;
top: 0;
white-space: nowrap;
padding: 10px;
margin-left: 10px;
background-color: #fff;
display: none;
}
.popover::before {
position: absolute;
border:10px solid transparent;
border-right-color:red;
right:100%;
top:5px;
content:''
}
.popover::after {
position: absolute;
border:10px solid transparent;
border-right-color:white;
right:100%;
top:5px;
content:'';
margin-right: -1px;
}
给按钮添加事件:
clickMe.addEventListener('click',function(){
popover.style.display = 'block'
})
按钮点击之后,浮层出现。
如果要设置成点击别处之后浮层消失,该怎么操作?
思路1:监听body,当body被点击后,浮层消失
document.body.addEventListener('click',function(){
console.log(1)
popover.style.display = 'none'
})
点击图中的空白区域,完全没有响应:
解决方法:给我们的body添加一个border,看看我们的点击区域是不是在body内:
body {border:1px solid red}
果然,因为body的高度是由其内部文档流元素的总体高度决定的,所以body的高度如图所示。我们只有点击了红色框内的范围,点击事件才会响应。
所以监听body是有可能会因为body高度不够而达不到需要的效果的。
思路2:监听document
修改代码:
document.addEventListener('click',function(){
console.log(1)
popover.style.display = 'none'
})
可以看到无论点击哪里,1都会被打印出来。
但是问题又来了:我们的点击按钮失效了。
事件传播过程
我们知道,事件传播顺序是先执行捕获,再执行冒泡,如果addEventListener()没有指定第三个参数为true或者false,则默认为false,即表示事件的回调函数会在事件冒泡阶段执行。
在这个过程中,两个事件都没有指定第三个参数,即都为false。当监听队列从document捕获到button,他们并没有做出反应,因为他们的回调函数不是在事件捕获阶段执行的;当从button冒泡到document时,他们的回调函数才会被触发,所以是先执行button的回调函数,运行popover.style.display = 'block'
,再执行document的回调函数,执行popover.style.display = 'none'
,他的样式是先被显示,然后马上被隐藏,所以看起来像是点击按钮失效了。
我们可以验证这一过程
clickMe.addEventListener('click',function(){
console.log(2)
popover.style.display = 'block'
})
document.addEventListener('click',function(){
console.log(1)
popover.style.display = 'none'
})
看到控制台先打印出2,再打印出1。
阻止传播
可以用阻止传播来解决这个问题
clickMe.addEventListener('click',function(e){
popover.style.display = 'block'
e.stopPropagation()
})
浮层就出来了:
没错,看似可以接受的方案,问题又来了,如果点击按钮后出现的这个浮层,它里面有内容,而我们又想选择里面的内容(比如一个CheckBox),但是这个浮层现在一点击就会消失,该怎么办呢?
让他的父元素阻止冒泡
wrapper.addEventListener('click',function(e){
e.stopPropagation()
})
这样我们就可以点击浮层而不让他消失了。
优化
- 阻止冒泡,并且只有当按钮被点击之后document才添加对点击事件的的一次监听,document对于其他点击事件不监听,节省内存。
$(clickMe).on('click',function(){
$(popover).show()
$(document).one('click',function(){
$(popover).hide()
})
})
$(wrapper).on('click',function(e){
e.stopPropagation()
})
- 或者添加setTimeout(),等一会(冒泡过程结束后)再把document的click事件添加进去:
$(clickMe).on('click',function(){
$(popover).show()
console.log('show')
setTimeout(function(){
console.log('添加one click')
$(document).one('click',function(){
console.log('点击按钮后这里不会被执行,因为冒泡通知过程已经结束')
$(popover).hide()
})
},0)
})
总结:
W3C制定了统一的标准-先捕获再冒泡
addEventListener有三个参数:element.addEventListener(event, function, useCapture)
- 第一个参数是需要绑定的事件
- 第二个参数是触发事件后要执行的函数
- 第三个参数默认值是false,表示在事件冒泡阶段调用事件处理函数;如果参数为true,则表示在事件捕获阶段调用处理函数。
传播过程可以用stopPorpagation()来阻止(但是要注意选择合适的节点使用)
本人文笔不好,关于事件的更详细的说明可以参考segmentfault上的这篇博文。