上周有几天是在写一个响应式网站,在写到移动端交互时.遇到一个问题,就是点击下拉框的选项时,下拉框背后的元素也被点击了.其实这个就是著名的点击穿透现象,所以趁着周末的时间把这个问题梳理了一下.而后呢,也是参考了一些文章以后整理了这篇总结,也算是本身对这个问题的一个记录吧.全部参考连接都放在文末.你们有兴趣的话,也能够去看看.css
300ms
延迟的由来,是当初07年初苹果发布首款iPhone以前,苹果工程师提出的一个为了优化交互体验的操做.由于当时的网站基本都是为PC等大屏幕设备而写的,而如今须要用小屏幕浏览桌面端网站.当用户用手指把页面放大之后,就有了一个双击缩放(double tap to zoom)的交互.即在iOS自带的Safari浏览器中快速双击会将网页缩放到原始比例.所以在此浏览器中,用户会有单击或者双击的需求行为.而浏览器并不能马上判断用户是想要单击仍是双击.因而乎,Safari就等待了300ms.看看用户到底想干吗.鉴于苹果公司这个操做的成功,后续其余的浏览器也所以借鉴了这种行为,而300ms也所以成为了大多数浏览器的一个约定.在当初移动端兴起的时候,300ms是可让人接受的,可是随着用户对交互体验的要求愈来愈高,300ms就成为了用户没法忍受的一个点了.html
上面简单的介绍了下300ms延迟产生的缘由.那么在移动端开发中,该怎么解决click事件的300ms延迟呢?目前网上的解决方案也较多,我也按照那些方案作了下测试,整理以下:vue
禁用浏览器的缩放,就是给咱们的页面里面加个meta头部,代表这个网页是不可缩放的.在App Store下载了几个浏览器试了下,夸克浏览器和QQ浏览器测试是有效果的.Safari,Chrome,Firefox 都不行.这个测试结果和我在一些文章里面看的不同,这是为啥啊?css3
<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
复制代码
更改默认的视口宽度,这下除了safari浏览器还有300ms之外,其余的浏览器都已经没有延迟了.貌似是由于这个方案尚未被safari经过.由于咱们已经为用户适配了页面大小和阻止了用户缩放,因此浏览器就不用再判断用户双击缩放了,因而便自动取消了click事件的300ms延迟git
<meta name="viewport" content="width=device-width" />
复制代码
设置 touch-action
属性,该设置会禁用掉该元素上的浏览器代理的任何默认行为,包括缩放,移动,拖拽等.它把全部的触摸类型的交互事件都禁止掉了,致使页面也不能滚动.感受在稍微复杂点的实际开发中,应该不会这么设置吧.github
html {
touch-action: none;
}
复制代码
1fastclick
原理: 在检测到 touchend 事件的时候,经过DOM自定义事件当即模拟一个click事件,并把浏览器本来300ms以后的click事件阻止掉.缺点是脚本相对较大,且有时候可能会有bug. 项目地址 : fastclick浏览器
既然click事件屁事这么多,那能不能用touch事件来代替click事件呢?答案是不能.假如咱们用 touchstart
事件来代替,一方面在用户手指触摸屏幕的时候就能触发,但有时候用户并不想点击,只想单纯的滑动屏幕而已.另外一方面,就是在某些场景下可能会出现点击穿透现象(ghost click).app
页面俩元素A和B,B在A的上面.在B的上面注册了touchstart事件,回调函数中是让B元素隐藏.当咱们点击B元素的时候,除了B被隐藏外,A的click事件也被触发了.这是由于在移动端浏览器中,事件执行顺序是 touchstart
=> touchmove
=> touchend
=> click
.click是有300ms的延迟.当B的touchstart事件触发后,B被隐藏了,300ms以后,浏览器触发了click事件,此时的事件已经被派发到了A元素身上.框架
移动端的事件是touch
事件,也叫触摸事件,由于是用手指触摸的.固然,点击事件仍是存在的. 咱们在上面提到了,在移动端浏览器中,事件执行顺序是 touchstart
=> touchmove
=> touchend
=> click
.下面咱们就先来介绍下这是个啥.函数
click事件是在最后执行的.通常状况下,click是在手指放到屏幕上,而且没有移动过,而后离开,且这个开始触摸到手指离开屏幕的时间较短才能触发,若手指移动了,则不会触发click事件了(看到有的地方说某些浏览器会容许有一个很小的移动值,具体的状况不太清楚). 所以正确的触发顺序是下面两个中的一个:
touchmove
,可能不会触发,也可能触发不少次,若触发了touchmove,则click就不会触发了. 咱们在Chrome浏览器的手机模式下运行下面的代码来验证上面的结论.
<<button id="btn">click me</button>
<div id="app"></div>
复制代码
let btn = document.querySelector('#btn')
let app = document.querySelector('#app')
let s = ''
btn.addEventListener('click', function(){
s += 'click '
app.innerText = s
})
btn.addEventListener('touchstart', function(e){
s += 'touchstart '
app.innerText = s
// e.preventDefault()
})
btn.addEventListener('touchmove', function(){
s += 'touchmove '
app.innerText = s
})
btn.addEventListener('touchend', function(){
s += 'touchend '
app.innerText = s
})
复制代码
咱们在按钮上单纯的点击一下,会打印出touchstart touchend click
.在按钮上快速的从左滑到右,会打印出touchstart touchmove touchmove touchmove touchmove touchend
,这里的touchmove
的数量不定.
和 click
等事件同样,touch
事件也是有事件对象的.touchstart
和 touchmove
经过 event.touched
来获取手指信息(好比触摸的点的位置等信息).可是 touchend
不能经过 event.touched
来获取.由于此时的手指已经离开了.可是能够经过 event.changedTouches
来获取手指信息.
解决点击穿透的方案也有好几个,你们能够根据本身项目的实际状况选择对应的解决方案.
新增一个看不见的元素阻止事件穿透.解决思路基本就是在触发事件的位置动态生成一个新的透明元素,这样当300ms以后的click事件来临时,点到的就是这个透明元素,而后再把这个元素删除便可.缺点就是写法麻烦,并且有时候用户快速点击的时候,下面元素的事件有可能不会触发,由于此时的透明元素尚未被定时器清理掉.固然还有一个方案和这个很像,那就是弄一个隐藏动画,时间大于300ms,在延迟以后的点击事件来临时,上面的元素尚未消失,这样就不会点到下面的元素了.
<div class="div1"></div>
<div class="div2"></div>
复制代码
.div1 {
width: 200px;
height: 200px;
background-color: pink;
}
.div2 {
width: 200px;
height: 200px;
background-color: orange;
position: relative;
top: -100px;
display: block;
opacity: 1;
}
复制代码
let div1 = document.querySelector('.div1')
let div2 = document.querySelector('.div2')
div1.addEventListener('click',function(){
console.log('div1')
})
div2.addEventListener('touchstart',function(e){
console.log('div2')
let el = document.createElement('div')
el.style.width = '200px'
el.style.height = '200px'
el.style.opacity = '0'
el.style.position = 'relative'
el.style.top = '-100px'
document.body.appendChild(el)
this.style.display = 'none'
setTimeout(() => {
document.body.removeChild(el)
}, 400)
})
复制代码
使用 event.preventDefault()
,把上面的代码改为
div2.addEventListener('touchstart',function(){
console.log('div2')
this.style.display = 'none'
e.preventDefault()
})
复制代码
使用 pointer-events
,这是一个css3中的属性.其中咱们用的比较多的属性值有auto
和none
.其余的属性基本都是为SVG服务的,戳我查看该属性详细介绍.
初始值就是auto
,适用于全部的元素,其表现效果和 pointer-events
属性未指定时同样.鼠标不会穿透当前层. none
则该元素不会成为鼠标事件的目标.鼠标再也不监听当前层而去监听下面层中的元素.可是若它的子元素设置了 pointer-events
为其余值,则鼠标仍是会监听这个子元素的.运行下面代码,观察效果.
.div1 {
width: 200px;
height: 200px;
background-color: orange;
}
.div2 {
width: 100px;
height: 100px;
background-color: pink;
pointer-events: none;
}
.div3 {
width: 50px;
height: 50px;
background-color: #00BFFF;
pointer-events: auto;
}
复制代码
<div class="div1">
div1
<div class="div2">
div2
<div class="div3">
div3
</div>
</div>
</div>
复制代码
let div1 = document.querySelector('.div1')
let div2 = document.querySelector('.div2')
let div3 = document.querySelector('.div3')
div1.addEventListener('click',function(){
console.log('div1')
})
div2.addEventListener('click',function(){
console.log('div2')
})
div3.addEventListener('click',function(){
console.log('div3')
})
复制代码
因此这里咱们解决点击穿透的方法就是,先设置下层元素的 pointer-events
为 none
,而后设置一个定时器,在必定的时间后将下层元素的属性值改成auto
便可.
引入 fastclick
库.
<script src="https://lib.baomitu.com/fastclick/1.0.6/fastclick.min.js"></script>
复制代码
下面是原生JS中的写法,在不一样的库(如jQuery)或者框架(如vue)中的写法都不太同样.不过基本都是大同小异,网上一搜一大堆,你们用到的时候去搜索下就好了.
if('addEventListener' in document){
document.addEventListener('DOMContentLoaded', function(){
FastClick.attach(document.body)
}, false)
}
复制代码
既然这回遇到了300ms以及点击穿透这样的问题,那就索性研究记录下你们的解决方案是啥.下次再遇到相似的问题基本就是成竹在胸了.下面是我参考的一些文章连接列表: