Zerlinda's Blog

click事件在移动端的“点透”问题

一.click与300ms延迟

快速响应是所有 UI 实现的重中之重。研究表明,当延迟超过 100 毫秒,用户就能感受到界面的卡顿。 然而,出于对手指触摸滑动的区分,移动端页面对于触摸事件会有 300 毫秒的延迟,导致多数用户感觉移动设备上基于 HTML 的 web 应用界面响应速度慢。

300 毫秒延迟的来历,要追溯至 2007 年初。苹果公司在发布首款 iPhone 前夕,遇到一个问题 —— 当时的网站都是为大屏幕设备所设计的。于是苹果的工程师们做了一些约定,应对 iPhone 这种小屏幕浏览桌面端站点的问题。这当中最出名的,当属双击缩放(double tap to zoom)。用户碰触页面之后,需要等待一段时间来判断是不是双击(double tap)动作,而不是立即响应单击(click),等待的这段时间大约是300ms。300ms的延迟就来自这里。

移动事件提供了 touchstart 、 touchmove 、 touchend 却没有提供tap支持,主流框架(库)都是手动实现了自定义tap事件,以求消除300ms延迟,提高页面响应速度。对于简单的页面,可以把 touchstart 或者 touchend 当作tap来用,但存在一些问题,比如手指接触目标元素,按住不放,慢慢移出响应区域,会触发 touchstart 事件执行对应的事件处理器(本不应该触发), touchend 事件也存在类似的问题。

此外, 使用原生touch事件也存在点击穿透的问题 ,因为click是在touch系列事件发生后大约300ms才触发的,混用touch和click肯定会导致点透问题。

二、点击穿透的场景

有了以上的基础,我们就可以理解为什么会出现点击穿透现象了。我们经常会看到“弹窗/浮层”这种东西,看以下demo。

.click与300ms延迟点透 

整个容器里有一个底层元素的div,和一个弹出层div,为了让弹出层有模态框的效果,添加一个遮罩层。

<div class="container">
    <div id="underLayer">底层元素</div>
    <div id="popupLayer">
        <div class="layer-title">弹出层</div>
        <div class="layer-action">
            <button class="btn" id="closePopup">关闭</button>
        </div>
    </div>
</div>
<div id="bgMask"></div>

然后为底层元素绑定 click 事件,而弹出层的关闭按钮绑定 tap 事件。

$('#closePopup').on('touch', function(e){
    $('#popupLayer').hide();
    $('#bgMask').hide();
});
$('#underLayer').on('click', function(){
    alert('underLayer clicked');
});

点击关闭按钮,touchend首先触发touch,弹出层和遮罩就被隐藏了。touchend后继续等待300ms发现没有其他行为了,则继续触发click,由于这时弹出层已经消失,所以当前click事件的target就在底层元素上,于是就alert内容。

由于click事件的滞后性(300ms),在这300ms内上层元素隐藏或消失了,下层同样位置的DOM元素触发了click事件(如果是input框则会触发focus事件),看起来就像点击的target“穿透”到下层去了。

由此总结点击穿透现象有3种:

1、点击穿透问题:点击蒙层(mask)上的关闭按钮,蒙层消失后发现触发了按钮下面元素的click事件。

蒙层的关闭按钮绑定的是touch事件,而按钮下面元素绑定的是click事件,touch事件触发之后,蒙层消失了,300ms后这个点的click事件fire,event的target自然就是按钮下面的元素,因为按钮跟蒙层一起消失了

2、跨页面点击穿透问题:如果按钮下面恰好是一个有href属性的a标签,那么页面就会发生跳转

因为 a标签跳转默认是click事件触发 ,所以原理和上面的完全相同

3、另一种跨页面点击穿透问题:这次没有mask了,直接点击页内按钮跳转至新页,然后发现新页面中对应位置元素的click事件被触发了

和蒙层的道理一样,js控制页面跳转的逻辑如果是绑定在touch事件上的,而且新页面中对应位置的元素绑定的是click事件,而且页面在300ms内完成了跳转,三个条件同时满足,就出现这种情况了

三、穿透的解决办法

1. 遮挡

由于 click 事件的滞后性,在这段时间内原来点击的元素消失了,于是便“穿透”了。因此我们顺着这个思路就想到,可以给元素的消失做一个fade效果,类似jQuery里的fadeOut,并设置动画duration大于300ms,这样当延迟的 click 触发时,就不会“穿透”到下方的元素了。

同样的道理,不用延时动画,我们还可以动态地在触摸位置生成一个透明的元素,这样当上层元素消失而延迟的click来到时,它点击到的是那个透明的元素,也不会“穿透”到底下。在一定的timeout后再将生成的透明元素移除。具体可见demo

2. pointer-events

pointer-events是CSS3中的属性,它有很多取值,具体的介绍请点击pointer-events-被忽略的CSS3属性。其中有用的主要是auto和none,其他属性值为SVG服务。

取值  含义
auto 效果和没有定义 pointer-events 属性相同,鼠标不会穿透当前层。
none 元素不再是鼠标事件的目标,鼠标不再监听当前层而去监听下面的层中的元素。但是如果它的子元素设置了pointer-events为其它值,比如auto,鼠标还是会监听这个子元素的。

因此解决“穿透”的办法就很简单,demo如下

$('#closePopup').on('tap', function(e){
    $('#popupLayer').hide();
    $('#bgMask').hide();
    $('#underLayer').css('pointer-events', 'none');
    setTimeout(function(){
        $('#underLayer').css('pointer-events', 'auto');
    }, 400);
});

3. fastclick

使用fastclick库,其实现思路是,取消 click 事件,用 touchend 模拟快速点击行为。点击此处下载fastclick

	FastClick.attach(document.body);

从此所有点击事件都使用click,不会出现“穿透”的问题,并且没有300ms的延迟。

 

 

发表评论

电子邮件地址不会被公开。 必填项已用*标注