js事件处理机制和popover轮子
在造轮子的时候,需要实现一个简单的点击按钮内容显示/隐藏,点击空白区域内容隐藏,点击内容区域内容不隐藏的功能
代码如下:
<div id="app" style="padding: 100px">
<g-popover>
<template slot="content">
<div>popover内容</div>
</template>
<button>点我</button>
</g-popover>
<g-popover>
<template slot="content">
<div>popover内容</div>
</template>
<button>点我</button>
</g-popover>
</div>
<script>
let app = new Vue({
el: '#app',
components: {
'g-popover': {
template: `
<div class="popover" @click="toggle">
<div class="content-wrapper" v-if="visibility">
<slot name="content"></slot>
</div>
<slot></slot>
</div>
`,
data(){
return {
visibility: false
}
},
methods: {
toggle(){
this.visibility = !this.visibility
}
}
}
}
})
</script>
1. 通过上面代码我们可以实现点击按钮显示和隐藏内容,但是如何实现点击空白区域内容隐藏哪?
toggle(){
this.visibility = !this.visibility
+ console.log('切换 visibility')
+ if(this.visibility){
+ document.body.addEventListener('click',()=>{
+ this.visibility = false
+ console.log('点击body就关闭popover')
+ })
+ }
}
上面的代码在点击事件里直接判断,如果visibility是true的话,就给让body监听click事件,当你点击body的时候,就隐藏。但是当你运行代码的时候,你发现不管你怎么点击按钮内容都不会再显示了,主要就是因为冒泡,当你一次点击的时候上面的事件就依次都执行了,也就是说'切换 visibility'和'点击body就关闭popover'都会打印出来,而不是等到你下一次点击body的时候才去隐藏。
2. 那么对于这个问题我们该怎么解决那?
我们可以通过异步来解决
if(this.visibility){
+ this.$nextTick(()=>{
document.body.addEventListener('click',()=>{
this.visibility = false
console.log('点击body就关闭popover')
})
+ })
}
问题1:body高度不是全页面高度,在下方点击无效
解决方法去监听document
document.addEventListener('click',()=>{
this.visibility = false
console.log('点击body就关闭popover')
})
问题2:第一次点击完按钮点空白区域可以正常显示隐藏,但第二次点击按钮,内容就不会显示,原因是因为你每一次点击按钮都会给document上添加一个click事件。
解决办法:每次新增前就把之前的click事件给关掉
this.$nextTick(()=>{
document.addEventListener('click',()=>{
this.visibility = false
+ document.removeEventListener('click',要删除的对应事件函数名)
console.log('点击body就关闭popover')
})
})
问题3:上面因为你要remove所以必须有对应的函数名,而你的assEventListener里的函数是箭头函数没有名字,所以你需要把箭头函数,换成普通函数
this.$nextTick(()=>{
document.addEventListener('click',function x(){
this.visibility = false
document.removeEventListener('click',x)
console.log('点击body就关闭popover')
})
})
问题4:上面的代码并没有起到效果,我们点击空白区域,内容并没有隐藏,原因是this指向改变了,我们用document监听click事件,因为里面用的是一个具名函数所以this最后指向的是document,这时候你的this.visibility就不会起作用。
解决办法:给你的函数绑定this
this.$nextTick(()=>{
document.addEventListener('click',function x(){
this.visibility = false
document.removeEventListener('click',x)
console.log('点击body就关闭popover')
}.bind(this))
})
小技巧:
()=>{}
等价于
function(){}.bind(this)
问题5:通过上面的代码我们又回到了问题二的状态,当点击完一次按钮,再点击空白区域后,再次点击按钮,内容就不会再显示了,主要原因是我们上面的removeEventListener并没有成功,因为我们监听的是function x(){...}.bind(this)
,而我们移除的是function x(){}
,这是两个不同的函数,就相当于x()和y=x.bind()。
解决办法:一开始就定义一个函数,然后传进去
this.$nextTick(()=>{
let x = ()=>{
this.visibility = false
document.removeEventListener('click',x)
console.log('点击body就关闭popover')
}
document.addEventListener('click',x)
})
3. 解决点击内容区域,内容隐藏问题
if(this.visibility){
this.$nextTick(()=>{
let x = ()=>{
this.visibility = false
console.log('document隐藏popover')
document.removeEventListener('click',x)
}
document.addEventListener('click',x)
})
}else{
console.log('vm隐藏popover')
}
问题1:上面的代码,当我们点击按钮隐藏内容的时候,它会打印出'document隐藏popover'和'vm隐藏popover',也就是说不但触发了自己本身的事件还触发了document的事件,这就是因为事件冒泡的原因:。
解决方法:我们不想让他冒泡,就需要在这个元素本身的点击事件上加一个.stop修饰符
<div class="popover" @click.stop="toggle">
这样每次就只会执行它本身的事件
问题2:
我们不想点击内容区域让它自己隐藏
解决办法:给当前内容区域加一个事件.stop
<div class="content-wrapper" v-if="visibility" @click.stop>
使用stop的问题
在使用组件的时候,在他们父元素上同样定义一个点击事件,触发的时候打印出'yyy'
<div style="overflow: hidden; border:1px solid black;padding: 10px;"
@click="yyy"
>
<g-popover>
<template slot="content">
<div>popover内容</div>
</template>
<button>点我</button>
</g-popover>
<g-popover>
<template slot="content">
<div>popover内容</div>
</template>
<button>点我</button>
</g-popover>
</div>
<scirpt>
var app = new Vue({
methods: {
yyy(){
console.log('yyy')
}
}
})
</script>
我们会发现当我们点击按钮的时候并不会触发yyy这个事件,但是我们点击黑色框内部空白区域就可以执行yyy事件,这是因为stop打断了用户的事件链。
不使用阻止冒泡来解决点击只执行当前事件的问题
方法:通过给点击事件传入一个原生的event参数,然后通过event.target拿到当前元素,之后给button一个ref,然后判断这个ref里包没包含event.target就可以知道是不是点击了按钮本身
<template>
<div class="popover" @click="toggle">
<div ref="content" class="content-wrapper" v-if="visibility">
<slot name="content"></slot>
</div>
<span ref="button">
<slot></slot>
</span>
</div>
</template>
<script>
export default {
name: 'GuluPopover',
data(){
return {
visibility: false
}
},
methods: {
toggle(e){
//这里的e.target就是<button></button>
if(this.$refs.button.contains(e.target)){
this.visibility = !this.visibility
console.log('点击了按钮')
}else{
console.log('点击了按钮外的')
}
}
}
运行上面代码,当你点击按钮的时候弹出'点击了按钮',当你点击内容的时候就会弹出‘点击了按钮外’,这样我们就可以实现,点击内容区域不隐藏内容了。
在上面代码的基础上再次实现点击document,内容隐藏
toggle(e){
//说明点击了按钮
if(this.$refs.button.contains(e.target)){
this.visibility = !this.visibility
console.log('点击了按钮')
if(this.visibility){
this.$nextTick(()=>{
document.body.appendChild(this.$refs.content)
let {left, top} = this.$refs.button.getBoundingClientRect()
this.$refs.content.style.left = left + window.scrollX + 'px'
this.$refs.content.style.top = top + window.scrollY + 'px'
let x = ()=>{
this.visibility = false
console.log('document隐藏popover')
document.removeEventListener('click',x)
}
document.addEventListener('click',x)
})
}else{
console.log('vm隐藏popover')
}
}else{
console.log('点击了按钮外的')
}
}
上面虽然点击document可以隐藏内容,但是同样点击内容本身也会隐藏,这还是因为冒泡,我们可以再次利用原生event.target来判断是否点击了内容区域
let x = (e)=>{
if(this.$refs.content.contains(e.target)){
//说明点击了内容区域
}else {
this.visibility = false
console.log('document隐藏popover')
document.removeEventListener('click',x)
}
}
对上面的代码进行重构
methods: {
positionContent(){
document.body.appendChild(this.$refs.content)
let {left, top} = this.$refs.button.getBoundingClientRect()
this.$refs.content.style.left = left + window.scrollX + 'px'
this.$refs.content.style.top = top + window.scrollY + 'px'
},
listenToDocument(){
let x = (e)=>{
if(this.$refs.content.contains(e.target)){
}else {
this.visibility = false
document.removeEventListener('click',x)
}
}
document.addEventListener('click',x)
},
onShow(){
this.$nextTick(()=>{
this.positionContent()
this.listenToDocument()
})
},
toggle(e){
//说明点击了按钮
if(this.$refs.button.contains(e.target)){
this.visibility = !this.visibility
if(this.visibility){
this.onShow()
}
}
}
}
查看我们上面的代码,点击按钮会有几次关闭
listenToDocument(){
let x = (e)=>{
if(this.$refs.content.contains(e.target)){
}else {
this.visibility = false
console.log('关闭')
document.removeEventListener('click',x)
}
}
document.addEventListener('click',x)
},
onShow(){
this.$nextTick(()=>{
this.positionContent()
this.listenToDocument()
})
},
toggle(e){
//说明点击了按钮
if(this.$refs.button.contains(e.target)){
this.visibility = !this.visibility
if(this.visibility){
this.onShow()
}else{
console.log('关闭')
}
}
}
问题1:当你运行上面代码时,你点击按钮显示的时候正常,但是再次点击按钮关闭的时候,你就会发现它会弹出两次关闭,这还是因为事件冒泡的原因
解决办法:和之前一样通过event,给popover这一个大的根元素加一个ref,只要是点击了这个popover里面的任何一个元素它都不会去执行document的事件
<template>
<div class="popover" @click="toggle" ref="popover">
<div ref="content" class="content-wrapper" v-if="visibility">
<slot name="content"></slot>
</div>
<span ref="button">
<slot></slot>
</span>
</div>
</template>
<script>
listenToDocument(){
let x = (e)=>{
if(this.$refs.popover.contains(e.target)){
return
}
//下面这句是因为当点击按钮的时候内容就被移出到body里了
if(this.$refs.content.contains(e.target)) return
this.visibility = false
console.log('关闭')
document.removeEventListener('click',x)
}
document.addEventListener('click',x)
}
</script>
这样再次运行,点击按钮隐藏就只会执行一次关闭
问题2:当你点击按钮多次切换内容显示隐藏的时候,再次点击document就会多次执行关闭
原因是当你点击按钮隐藏内容的时候,你的document监听并未移除,通过给监听前后加console.log就可以知道
listenToDocument(){
let x = (e)=>{
if(this.$refs.popover.contains(e.target)){
return
}
this.visibility = false
console.log('关闭')
+ console.log('结束监听document')
document.removeEventListener('click',x)
}
+ console.log('监听document')
document.addEventListener('click',x)
}
当你点击按钮显示内容后,再次点击按钮隐藏内容,这时候你会发现你并未移除你的document的监听事件
这时候如果你再次点击按钮显示内容的话,document就会再次添加一次监听,当你再点击document的时候它就会把所有的监听都结束
这时候再点击document,上面两次监听事件会一起结束
解决办法:把关闭入口收拢,也就是说把所有关闭的代码写在一个函数里,当关闭这个函数执行的时候,就去移除document的事件监听
methods: {
positionContent(){
document.body.appendChild(this.$refs.content)
let {left, top} = this.$refs.button.getBoundingClientRect()
this.$refs.content.style.left = left + window.scrollX + 'px'
this.$refs.content.style.top = top + window.scrollY + 'px'
},
//这里为了让下面使用这个函数,所以在方法中声明
x(e){
if(this.$refs.popover.contains(e.target)){
return
}
this.close()
},
listenToDocument(){
console.log('监听document')
document.addEventListener('click',this.x)
},
//我们收拢的关闭函数
close(){
this.visibility = false;
console.log('关闭')
//每次关闭的时候都移除document监听
document.removeEventListener('click',this.x)
},
open(){
this.visibility = true
this.$nextTick(()=>{
this.positionContent()
this.listenToDocument()
})
},
toggle(e){
//说明点击了按钮
if(this.$refs.button.contains(e.target)){
if(this.visibility === true){
this.close()
}else{
this.open()
}
}
}
}
这样不管我们点击多少遍按钮再点击document它只会关闭一遍,每次关闭都会移除监听