# 移动端300ms延迟以及点击穿透
上周有几天是在写一个响应式网站,在写到移动端交互时.遇到一个问题,就是点击下拉框的选项时,下拉框背后的元素也被点击了.其实这个就是著名的点击穿透现象,因此趁着周末的时间把这个问题梳理了一下.然后呢,也是参考了一些文章之后整理了这篇总结,也算是自己对这个问题的一个记录吧.所有参考链接都放在文末.大家有兴趣的话,也可以去看看.
# 300ms延迟
# 延迟产生原因
300ms
延迟的由来,是当初07年初苹果发布首款iPhone之前,苹果工程师提出的一个为了优化交互体验的操作.因为当时的网站基本都是为PC等大屏幕设备而写的,而现在需要用小屏幕浏览桌面端网站.当用户用手指把页面放大以后,就有了一个双击缩放(double tap to zoom)的交互.即在iOS自带的Safari浏览器中快速双击会将网页缩放到原始比例.因此在此浏览器中,用户会有单击或者双击的需求行为.而浏览器并不能立刻判断用户是想要单击还是双击.于是乎,Safari就等待了300ms.看看用户到底想干嘛.鉴于苹果公司这个操作的成功,后续其他的浏览器也因此借鉴了这种行为,而300ms也因此成为了大多数浏览器的一个约定.在当初移动端兴起的时候,300ms是可以让人接受的,但是随着用户对交互体验的要求越来越高,300ms就成为了用户无法忍受的一个点了.
# 解决延迟
上面简单的介绍了下300ms延迟产生的原因.那么在移动端开发中,该怎么解决click事件的300ms延迟呢?目前网上的解决方案也较多,我也按照那些方案做了下测试,整理如下:
# 禁用缩放
禁用浏览器的缩放,就是给我们的页面里面加个meta头部,表明这个网页是不可缩放的.在App Store下载了几个浏览器试了下,夸克浏览器和QQ浏览器测试是有效果的.Safari,Chrome,Firefox 都不行.这个测试结果和我在一些文章里面看的不一样,这是为啥啊?
<meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
# 改变视口宽度
更改默认的视口宽度,这下除了safari浏览器还有300ms以外,其他的浏览器都已经没有延迟了.貌似是因为这个方案还没有被safari通过.因为我们已经为用户适配了页面大小和阻止了用户缩放,所以浏览器就不用再判断用户双击缩放了,于是便自动取消了click事件的300ms延迟
<meta name="viewport" content="width=device-width" />
# touch-action
设置 touch-action
属性,该设置会禁用掉该元素上的浏览器代理的任何默认行为,包括缩放,移动,拖拽等.它把所有的触摸类型的交互事件都禁止掉了,导致页面也不能滚动.感觉在稍微复杂点的实际开发中,应该不会这么设置吧.
html {
touch-action: none;
}
# 引用fastclick库
1fastclick
原理: 在检测到 touchend 事件的时候,通过DOM自定义事件立即模拟一个click事件,并把浏览器原本300ms之后的click事件阻止掉.缺点是脚本相对较大,且有时候可能会有bug. 项目地址 : fastclick
既然click事件屁事这么多,那能不能用touch事件来代替click事件呢?答案是不能.假如我们用 touchstart
事件来代替,一方面在用户手指触摸屏幕的时候就能触发,但有时候用户并不想点击,只想单纯的滑动屏幕而已.另一方面,就是在某些场景下可能会出现点击穿透现象(ghost click).
# 点击穿透
# 什么是点击穿透
页面俩元素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
.下面我们就先来介绍下这是个啥.
- touchstart 是手指放到屏幕上就触发
- touchmove 是手指在屏幕上滑动的时候触发
- touchend 是手指离开屏幕的时候触发
click事件是在最后执行的.一般情况下,click是在手指放到屏幕上,并且没有移动过,然后离开,且这个开始触摸到手指离开屏幕的时间较短才能触发,若手指移动了,则不会触发click事件了(看到有的地方说某些浏览器会允许有一个很小的移动值,具体的情况不太清楚). 因此正确的触发顺序是下面两个中的一个:
- touchstart => touchmove => touchend
- touchstart => touchend => 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
使用 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库
引入 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以及点击穿透这样的问题,那就索性研究记录下大家的解决方案是啥.下次再遇到类似的问题基本就是胸有成竹了.下面是我参考的一些文章链接列表:
- https://blog.csdn.net/fhjdzkp/article/details/100918720
- https://blog.csdn.net/lululove19870526/article/details/44345759
- https://www.jianshu.com/p/6e2b68a93c88
- https://developer.mozilla.org/zh-CN/docs/Web/CSS/pointer-events
- https://blog.csdn.net/zhuyinqinying/article/details/81775671
- https://zhuanlan.zhihu.com/p/87579573
- https://blog.csdn.net/thunderevil35/article/details/80587618
← deno入门指南 来了解下字符编码的历史吧 →