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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,245评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,749评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,960评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,575评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,668评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,670评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,664评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,422评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,864评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,178评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,340评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,015评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,646评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,265评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,494评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,261评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,206评论 2 352

推荐阅读更多精彩内容

  •   JavaScript 与 HTML 之间的交互是通过事件实现的。   事件,就是文档或浏览器窗口中发生的一些特...
    霜天晓阅读 3,485评论 1 11
  • 工厂模式类似于现实生活中的工厂可以产生大量相似的商品,去做同样的事情,实现同样的效果;这时候需要使用工厂模式。简单...
    舟渔行舟阅读 7,745评论 2 17
  • 第一部分 HTML&CSS整理答案 1. 什么是HTML5? 答:HTML5是最新的HTML标准。 注意:讲述HT...
    kismetajun阅读 27,466评论 1 45
  • 时间活过得真快,一周的学习又过去了,第一天签到学会了时间上的管理,第二天学会了用金,木,水,火,土,打如何打造好朋...
    段有群阅读 316评论 0 0
  • 问题:Windows下,Vim的配置文件(_vimrc)在哪? 环境:windows10, vim8.0 解决办法...
    小猫藏鱼阅读 15,082评论 0 2