一、click事件的300毫秒延迟是怎么产生的?
苹果在2007年发布iphone前夕遇到一个问题,当时相应的网页都是针对大屏编写的,如果直接在iphone上浏览则会出现横向的滚动条,于是他们制定了一些规则来缩放网页,通过双击可以还原网页大小,这种方法后来被其它移动浏览器厂商所采用。300毫秒延迟产生的原因就在于当用户一次点击屏幕之后,浏览器并不能立刻判断用户是确实要打开这个链接,还是想要进行双击操作。因此,iOS Safari 就等待 300 毫秒,以判断用户是否再次点击了屏幕。
相应的解决办法
1.禁用缩放
<meta name="viewport" content="width=device-width, user-scalable=no">
这个方案有一个缺点,就是必须通过完全禁用缩放来达到去掉点击延迟的目的,然而完全禁用缩放并不是我们的初衷,我们只是想禁掉默认的双击缩放行为,这样就不用等待300ms来判断当前操作是否是双击。但是通常情况下,我们还是希望页面能通过双指缩放来进行缩放操作,比如放大一张图片,放大一段很小的文字。
<meta name="viewport" content="width=device-width">
为了让桌面站点能在移动端浏览器正常显示,移动端浏览器默认的视口宽度并不等于设备浏览器视窗宽度,而是要比设备浏览器视窗宽度大,通常是980px。我们可以通过上面的标签来设置视口宽度为设备宽度。那浏览器就可以认为该网站已经对移动端做过了适配和优化,就无需双击缩放操作了。
这个方案相比方案一的好处在于,它没有完全禁用缩放,而只是禁用了浏览器默认的双击缩放行为,但用户仍然可以通过双指缩放操作来缩放页面。
3.css3的touch-action
touch-action
这个CSS属性。这个属性指定了相应元素上能够触发的浏览器的默认行为。文档说明
html { -ms-touch-action: manipulation; touch-action: manipulation; }
该方案只支持移动端的chromiun 和 iOS 9.3+
4、使用fast click库
fastclick的实现思路是使用touchend事件来模拟click,不推荐该方法,该方法在特定场景下会导致事件穿透。
FastClick.prototype.onTouchEnd = function(event){ // 一些状态监测代码 // 从这里开始, if (!this.needsClick(targetElement)) { // 如果这不是一个需要使用原生click的元素,则屏蔽原生事件,避免触发两次click event.preventDefault(); // 触发一次模拟的click this.sendClick(targetElement, event); } } FastClick.prototype.sendClick = function(targetElement, event) { // 这里是一些状态检查逻辑 // 创建一个鼠标事件 clickEvent = document.createEvent('MouseEvents'); // 初始化鼠标事件为click事件 clickEvent.initMouseEvent(this.determineEventType(targetElement), true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null); // fastclick的内部变量,用来识别click事件是原生还是模拟 clickEvent.forwardedTouchEvent = true; // 在目标元素上触发该鼠标事件, targetElement.dispatchEvent(clickEvent); }
二、事件穿透
1.现象描述
假如页面上有两个元素A和B。B元素在A元素之上。我们给B元素的touchstart或touchend绑定一个回调函数,该回调函数的作用是隐藏B元素,当触摸B元素,B元素被隐藏了,随后,A元素触发了click事件。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> <style> .mask{ width: 200px; height: 100px; background-color: gold; } .top{ width: 100px; height:100px; background-color: gray; position: absolute; top:0; left: 0; } </style> </head> <body> <div class="mask" id="bottom">我是底层</div> <div class="top" id="top">我是上层</div> <script> window.onload=function(){ document.getElementById("bottom").onclick=function(){ alert('我是底层'); } document.getElementById("top").ontouchend=function(){ this.style.display='none'; } } </script> </body> </html>
2.产生的条件
上层元素采用touchstart或touchend,下层元素有click事件或是一个指定了href的超链接,上层元素在触摸后不可见
3.产生的原因
在移动端浏览器,事件执行的顺序是touchstart > touchend > click,touch事件结束后会等待300毫秒,然后执行click事件。
上面的例子中touchend把上层元素隐藏之后,隔了300ms,浏览器触发了click事件,但是此时上层元素不见了,所以该事件被派发到了底层元素身上。如果底层元素是一个链接,那此时页面就会意外地跳转。
4.如何解决
网上搜到的meta头禁用缩放,css3的touch-action这两种方法都是瞎扯蛋,经实验,可行的方法如下:
4.1 统一使用touch事件( touchstart
、’touchend’),然后上层元素隐藏后使用event.preventDefault(),要不然下层元素如果是a标签,a标签的href也是click行为
4.2 如果不需要判断触摸手势等,可以把页面内所有的touch换成click,然后采用第一节中的方案来解决click延迟
4.3 遮挡法:动态地在触摸位置生成一个透明的元素,这样当上层元素消失而延迟的click来到时,它点击到的是那个透明的元素,也不会“穿透”到底下
4.4 延迟法:给消失的元素做一个fade效果,类似jQuery里的fadeOut,并设置动画duration大于300ms,这样当延迟的 click 触发时,就不会“穿透”到下方的元素了。
4.5下层元素使用pointer-events属性,上层元素隐藏时将下层元素的pointer-events设为none,然后400ms后把该属性恢复成auto。该方法的问题在于必须要知道可能受影响的下层元素有哪些,实际项目中意义不大
window.onload=function(){ document.getElementById("bottom").onclick=function(){ alert('我是底层'); } document.getElementById("top").ontouchend=function(e){ this.style.cssText="display:none;" var bottom=document.getElementById("bottom"); bottom.style.cssText="pointer-events:none"; setTimeout(function(){ bottom.style.cssText="pointer-events:auto"; }, 400); } }