js事件处理机制和popover轮子

2018-11-19  本文已影响21人  sweetBoy_9126

在造轮子的时候,需要实现一个简单的点击按钮内容显示/隐藏,点击空白区域内容隐藏,点击内容区域内容不隐藏的功能
代码如下:

<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它只会关闭一遍,每次关闭都会移除监听

上一篇 下一篇

猜你喜欢

热点阅读