zepto的tap点透问题

   https://segmentfault.com/a/1190000003848737

准备上传

穿透现象与click的延迟解决方法是分不开的,若要了解穿透现象,需要先了解click延迟的解决原理。

移动端click事件300ms的延迟现象的原因:

在最早iphone的safar浏览器中,为了实现触屏中双击放大效果,当用户点击屏幕时后会判断在300ms内是否有第二次点击,如果有,就理解成双击,若没有就是单击, 就会触发click事件. 当你点击移动设备的屏幕时, 可以分解成多个事件,顺序依次为:

touchstart — touchmove — touchend — click,
这些事件是按顺序依次触发的.

解决延迟的思路:

touchstart touchend是没有延迟的,可以在touchend时触发用户想要在click时触发的事件.

zepto 解决click延迟的原理:

自定义tap事件,当用户点击元素时,touchend事件先发生, 当touchend事件冒泡到document时触发目标元素绑定的tap事件

简单模拟zepto tap的实现方式(这里忽略touchstart与touchend的点击位置的判断):

// document元素上绑定touchend事件, 在touchend的事件处理函数中自定义tap事件, 当点击的目标元素的touchend事件冒泡到document上时, 触发绑定在目标元素上的tap事件
document.addEventListener('touchend', function(e) {
  // 自定义tap事件
  var evt = document.createEvent('Event');
  evt.init(’tap’, true, true);

  // 触发绑定在目标元素上的tap事件
  e.target.dispatch(evt);
}, false);
// -----------------------------------------------------------------
// 用户绑定tap事件
document.getElementById(‘elementid’).addEventListener('tap’, function(e) {
  // click事件逻辑
}, false);
复制代码

zepto的tap穿透现象:

touch事件的来源

PC网页上的大部分操作都是用鼠标的,即响应的是鼠标事件,包括mousedownmouseupmousemoveclick事件。一次点击行为,事件的触发过程为:mousedown -> mouseup -> click 三步。

手机上没有鼠标,所以就用触摸事件去实现类似的功能。touch事件包含touchstarttouchmovetouchend,注意手机上并没有tap事件。手指触发触摸事件的过程为:touchstart -> touchmove -> touchend

手机上没有鼠标,但不代表手机不能响应mouse事件(其实是借助touch去触发mouse事件)。有人在PC和手机上对事件做了对比实验,以说明手机对touch事件相应速度快于mouse事件。


<!DOCTYPE HTML>
<html>
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
	<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
	<title>点击穿透问题</title>

	<style type="text/css">
	body{
		margin: 0;
	}
	.container{
		width: 100%;
		overflow: hidden;
		position: relative;
	}
	.layer-title{
		width: 100%;
		margin: 50px 0;
		text-align: center;
	}
	.layer-action{
		position: absolute;
		bottom: 20px;
		width: 100%;
		text-align: center;
	}
	.btn{
		background-color: #08c;
		border: 0;
		color: #fff;
		height: 30px;
		line-height: 30px;
		width: 100px;
	}

	#underLayer{
		background-color: #eee;
		width: 90%;
		height: 500px;
		line-height: 500px;
		margin: 30px auto 1000px;
		text-align: center;
	}

	#popupLayer{
		/*display: none;*/
		background-color: #fff;
		width: 80%;
		height: 200px;
		position: fixed;
		top: 50%;
		left: 50%;
		margin-left: -40%;
		margin-top: -100px;
		z-index: 1;
	}

	#bgMask{
		position: fixed;
		top: 0;
		left: 0;
		right: 0;
		bottom: 0;
		background-color: rgba(0,0,0,0.6);
	}
	</style>
</head>
<body>
	<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>


	<script type="text/javascript" src=""></script>
	<script type="text/javascript">
	Zepto(function($){
		// 点击穿透
		var $close = $('#closePopup');
		var $popup = $('#popupLayer');
		var $under = $('#underLayer');
		var $mask = $('#bgMask');

		$close.on('tap', function(e){
			$popup.hide();
			$mask.hide();
		});

		$under.on('click', function(){
			alert('underLayer clicked');
		});

	});
	</script>
</body>
</html>
复制代码

遮罩层中有一个标签绑定了tap事件,触发时遮罩层消失,该标签正下方有一个绑定了click的按钮,此时点击上层的标签,同时也会触发下层元素的click事件,出现穿透的现象。

为什么会出现穿透:

结合前面tap事件的原理来分析:

当触发tap事件,上层遮罩层关闭后,此时事件只进行到touchend,而click是在大概300ms后才触发,当click触发时,上面的遮罩层已消失,就相当于点击到了下层的元素。

下层什么样的元素才会形成穿透:

根据原理来说,因为穿透是发生在click发生时,也就是下层绑定了click事件或click时会触发的事件(focus focusout)的元素,或点击时有默认形为的标签元素,如a input(会出系统键盘的type类型或绑定了focus事件)等。

如何解决穿透:

方法一:直接将上层元素的tap事件换成click事件(会出现300ms的延迟触发事件)

方法二:在click事件触发前阻止它,如在touchend的事件中使用e.preventDefault()来阻止后续的click事件

zepto为何不使用e.preventDefault()来解决穿透问题?

因为zepto的tap事件统一是在document的touchend时触发的,若在这里使用e.preventDefault(),那页面上所有元素在touchend后触发的事件都不会被执行了。



这是最近第四次发文说tap的点透事件,我们一直对解决“点透”的蒙版耿耿于怀,于是今天老大提出了一个库fastclick,最后证明解决了我们的问题

而且click不必替换为tap了,于是我们老大就语重心长的对我说了一句,你们就误我吧,我邮件都发出去了......

于是我下午就在看fastclick这个库,看看是不是能解决我们的问题,于是我们开始吧

读fastclick源码

尼玛使用太简单了,直接一句:

FastClick.attach(document.body);复制代码

于是所有的click响应速度直接提升,刚刚的!什么input获取焦点的问题也解决了!!!尼玛如果真的可以的话,原来改页面的同事肯定会啃了我

一步步来,我们跟进去,入口就是attach方法:

1 FastClick.attach = function(layer) {
2  'use strict';
3  return new FastClick(layer);
4 };复制代码

这个兄弟不过实例化了下代码,所以我们还要看我们的构造函数:

 1 function FastClick(layer) {
 2  'use strict';
 3  var oldOnClick, self = this;
 4  this.trackingClick = false;
 5  this.trackingClickStart = 0;
 6  this.targetElement = null;
 7  this.touchStartX = 0;
 8  this.touchStartY = 0;
 9  this.lastTouchIdentifier = 0;
10  this.touchBoundary = 10;
11  this.layer = layer;
12  if (!layer || !layer.nodeType) {
13   throw new TypeError('Layer must be a document node');
14  }
15  this.onClick = function() { return FastClick.prototype.onClick.apply(self, arguments); };
16  this.onMouse = function() { return FastClick.prototype.onMouse.apply(self, arguments); };
17  this.onTouchStart = function() { return FastClick.prototype.onTouchStart.apply(self, arguments); };
18  this.onTouchMove = function() { return FastClick.prototype.onTouchMove.apply(self, arguments); };
19  this.onTouchEnd = function() { return FastClick.prototype.onTouchEnd.apply(self, arguments); };
20  this.onTouchCancel = function() { return FastClick.prototype.onTouchCancel.apply(self, arguments); };
21  if (FastClick.notNeeded(layer)) {
22   return;
23  }
24  if (this.deviceIsAndroid) {
25   layer.addEventListener('mouseover', this.onMouse, true);
26   layer.addEventListener('mousedown', this.onMouse, true);
27   layer.addEventListener('mouseup', this.onMouse, true);
28  }
29  layer.addEventListener('click', this.onClick, true);
30  layer.addEventListener('touchstart', this.onTouchStart, false);
31  layer.addEventListener('touchmove', this.onTouchMove, false);
32  layer.addEventListener('touchend', this.onTouchEnd, false);
33  layer.addEventListener('touchcancel', this.onTouchCancel, false);
34 
35  if (!Event.prototype.stopImmediatePropagation) {
36   layer.removeEventListener = function(type, callback, capture) {
37    var rmv = Node.prototype.removeEventListener;
38    if (type === 'click') {
39     rmv.call(layer, type, callback.hijacked || callback, capture);
40    } else {
41     rmv.call(layer, type, callback, capture);
42    }
43   };
44 
45   layer.addEventListener = function(type, callback, capture) {
46    var adv = Node.prototype.addEventListener;
47    if (type === 'click') {
48     adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) {
49      if (!event.propagationStopped) {
50       callback(event);
51      }
52     }), capture);
53    } else {
54     adv.call(layer, type, callback, capture);
55    }
56   };
57  }
58  if (typeof layer.onclick === 'function') {
59   oldOnClick = layer.onclick;
60   layer.addEventListener('click', function(event) {
61    oldOnClick(event);
62   }, false);
63   layer.onclick = null;
64  }
65 }复制代码

看看这段代码,上面很多属性干了什么事情我也不知道......于是忽略了

1 if (!layer || !layer.nodeType) {
2  throw new TypeError('Layer must be a document node');
3 }复制代码

其中这里要注意,我们必须传入一个节点给构造函数,否则会出问题

然后这个家伙将一些基本的鼠标事件注册在自己的属性方法上了,具体是干神马的我们后面再说

在后面点有个notNeeded方法:

 1 FastClick.notNeeded = function(layer) {
 2  'use strict';
 3  var metaViewport;
 4  if (typeof window.ontouchstart === 'undefined') {
 5   return true;
 6  }
 7  if ((/Chrome\/[0-9]+/).test(navigator.userAgent)) {
 8   if (FastClick.prototype.deviceIsAndroid) {
 9    metaViewport = document.querySelector('meta[name=viewport]');
10    if (metaViewport && metaViewport.content.indexOf('user-scalable=no') !== -1) {
11     return true;
12    }
13   } else {
14    return true;
15   }
16  }
17  if (layer.style.msTouchAction === 'none') {
18   return true;
19  }
20  return false;
21 };复制代码

这个方法用于判断是否需要用到fastclick,注释的意思不太明白,我们看看代码吧

首先一句:

1 if (typeof window.ontouchstart === 'undefined') {
2   return true;
3 }复制代码

如果不支持touchstart事件的话,返回true
PS:现在的只管感受就是fastclick应该也是以touch事件模拟的,但是其没有点透问题

后面还判断了android的一些问题,我这里就不关注了,意思应该就是支持touch才能支持吧,于是回到主干代码

主干代码中,我们看到,如果浏览器不支持touch事件或者其它问题就直接跳出了

然后里面有个deviceIsAndroid的属性,我们跟去看看(其实不用看也知道是判断是否是android设备)

FastClick.prototype.deviceIsAndroid = navigator.userAgent.indexOf('Android') > 0;

绑定事件

好了,这家伙开始绑定注册事件了,至此还未看出异样

 1 if (this.deviceIsAndroid) {
 2  layer.addEventListener('mouseover', this.onMouse, true);
 3  layer.addEventListener('mousedown', this.onMouse, true);
 4  layer.addEventListener('mouseup', this.onMouse, true);
 5 }
 6 layer.addEventListener('click', this.onClick, true);
 7 layer.addEventListener('touchstart', this.onTouchStart, false);
 8 layer.addEventListener('touchmove', this.onTouchMove, false);
 9 layer.addEventListener('touchend', this.onTouchEnd, false);
10 layer.addEventListener('touchcancel', this.onTouchCancel, false);复制代码

具体的事件函数在前面被重写了,我们暂时不管他,继续往后面看先(话说,这家伙绑定的事件够多的)

stopImmediatePropagation

完了多了一个属性:

阻止当前事件的冒泡行为并且阻止当前事件所在元素上的所有相同类型事件的事件处理函数的继续执行.

如果某个元素有多个相同类型事件的事件监听函数,则当该类型的事件触发时,多个事件监听函数将按照顺序依次执行.如果某个监听函数执行了 event.stopImmediatePropagation()方法,则除了该事件的冒泡行为被阻止之外(event.stopPropagation方法的作用),该元素绑定的其余相同类型事件的监听函数的执行也将被阻止.

 1 <html>
 2     <head>
 3         <style>
 4             p { height: 30px; width: 150px; background-color: #ccf; }
 5             div {height: 30px; width: 150px; background-color: #cfc; }
 6         </style>
 7     </head>
 8     <body>
 9         <div>
10             <p>paragraph</p>
11         </div>
12         <script>
13             document.querySelector("p").addEventListener("click", function(event)
14             {
15                 alert("我是p元素上被绑定的第一个监听函数");
16             }, false);
17             document.querySelector("p").addEventListener("click", function(event)
18             {
19                 alert("我是p元素上被绑定的第二个监听函数");
20                 event.stopImmediatePropagation();
21                 //执行stopImmediatePropagation方法,阻止click事件冒泡,并且阻止p元素上绑定的其他click事件的事件监听函数的执行.
22             }, false);
23             document.querySelector("p").addEventListener("click", function(event)
24             {
25                 alert("我是p元素上被绑定的第三个监听函数");
26                 //该监听函数排在上个函数后面,该函数不会被执行.
27             }, false);
28             document.querySelector("div").addEventListener("click", function(event)
29             {
30                 alert("我是div元素,我是p元素的上层元素");
31                 //p元素的click事件没有向上冒泡,该函数不会被执行.
32             }, false);
33         </script>
34     </body>
35 </html>复制代码

 1 if (!Event.prototype.stopImmediatePropagation) {
 2  layer.removeEventListener = function(type, callback, capture) {
 3   var rmv = Node.prototype.removeEventListener;
 4   if (type === 'click') {
 5    rmv.call(layer, type, callback.hijacked || callback, capture);
 6   } else {
 7    rmv.call(layer, type, callback, capture);
 8   }
 9  };
10 
11  layer.addEventListener = function(type, callback, capture) {
12   var adv = Node.prototype.addEventListener;
13   if (type === 'click') {
14    adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) {
15     if (!event.propagationStopped) {
16      callback(event);
17     }
18    }), capture);
19   } else {
20    adv.call(layer, type, callback, capture);
21   }
22  };
23 }复制代码

然后这家伙重新定义了下注册与注销事件的方法,

我们先看注册事件,其中用到了Node的addEventListener,这个Node是个什么呢?

由此观之,Node是一个系统属性,代表我们的节点吧,所以这里重写了注销的事件

这里,我们发现,其实他只对click进行了特殊处理

1 adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) {
2  if (!event.propagationStopped) {
3   callback(event);
4  }
5 }), capture);复制代码

其中有个hijacked劫持是干神马的就暂时不知道了,估计是在中间是否改写的意思吧
然后这里重写写了下,hijacked估计是一个方法,就是为了阻止在一个dom上注册多次事件多次执行的情况而存在的吧

注销和注册差不多我们就不管了,到此我们其实重写了我们传入dom的注册注销事件了,好像很厉害的样子,意思以后这个dom调用click事件用的是我们的,当然这只是我暂时的判断,具体还要往下读,而且我觉得现在的判断不靠谱,于是我们继续吧

我们注销事件时候可以用addEventListener 或者 dom.οnclick=function(){},所以这里有了下面的代码:

1 if (typeof layer.onclick === 'function') {
2  oldOnClick = layer.onclick;
3  layer.addEventListener('click', function(event) {
4   oldOnClick(event);
5  }, false);
6  layer.onclick = null;
7 }复制代码

此处,他的主干流程居然就完了,意思是他所有的逻辑就在这里了,不论入口还是出口应该就是事件注册了,于是我们写个代码来看看

测试入口

 1 <input type="button" id="addEvent" value="addevent">
 2 <input type="button" id="addEvent1" value="addevent1">
 3 
 4 $('#addEvent').click(function () {
 5     var dom = $('#addEvent1')[0]
 6     dom.addEventListener('click', function () {
 7         alert('')
 8         var s = '';
 9     })
10 });复制代码

我们来这个断点看看我们点击后干了什么,我们现在点击按钮1会为按钮2注册事件:

但是很遗憾,我们在电脑上不能测试,所以增加了我们读代码的困难,在手机上测试后,发现按钮2响应很快,但是这里有点看不出问题
最后alert了一个!Event.prototype.stopImmediatePropagation发现手机和电脑都是false,所以我们上面搞的东西暂时无用

 1 FastClick.prototype.onClick = function (event) {
 2     'use strict';
 3     var permitted;
 4     alert('终于尼玛进来了');
 5     if (this.trackingClick) {
 6         this.targetElement = null;
 7         this.trackingClick = false;
 8         return true;
 9     }
10     if (event.target.type === 'submit' && event.detail === 0) {
11         return true;
12     }
13     permitted = this.onMouse(event);
14     if (!permitted) {
15         this.targetElement = null;
16     }
17     return permitted;
18 };复制代码

然后我们终于进来了,现在我们需要知道什么是trackingClick 了

1 /**
2 * Whether a click is currently being tracked.
3 * @type boolean
4 */
5 this.trackingClick = false;复制代码

我们最初这个属性是false,但是到这里就设置为true了,就直接退出了,说明绑定事件终止,算了这个我们暂时不关注,我们干点其它的,
因为,我觉得重点还是应该在touch事件上

PS:到这里,我们发现这个库应该不只是将click加快,而是所有的响应都加快了

我在各个事件部分log出来东西,发现有click的地方都只执行了touchstart与touchend,于是至此,我觉得我的观点成立
他使用touch事件模拟量click,于是我们就只跟进这一块就好:

 1 FastClick.prototype.onTouchStart = function (event) {
 2     'use strict';
 3     var targetElement, touch, selection;
 4     log('touchstart');
 5     if (event.targetTouches.length > 1) {
 6         return true;
 7     }
 8     targetElement = this.getTargetElementFromEventTarget(event.target);
 9     touch = event.targetTouches[0];
10     if (this.deviceIsIOS) {
11         selection = window.getSelection();
12         if (selection.rangeCount && !selection.isCollapsed) {
13             return true;
14         }
15         if (!this.deviceIsIOS4) {
16             if (touch.identifier === this.lastTouchIdentifier) {
17                 event.preventDefault();
18                 return false;
19             }
20             this.lastTouchIdentifier = touch.identifier;
21             this.updateScrollParent(targetElement);
22         }
23     }
24     this.trackingClick = true;
25     this.trackingClickStart = event.timeStamp;
26     this.targetElement = targetElement;
27     this.touchStartX = touch.pageX;
28     this.touchStartY = touch.pageY;
29     if ((event.timeStamp - this.lastClickTime) < 200) {
30         event.preventDefault();
31     }
32     return true;
33 };复制代码

其中用到了一个方法:

1 FastClick.prototype.getTargetElementFromEventTarget = function (eventTarget) {
2     'use strict';
3     if (eventTarget.nodeType === Node.TEXT_NODE) {
4         return eventTarget.parentNode;
5     }
6     return eventTarget;
7 };复制代码

他是获取我们当前touchstart的元素

然后将鼠标的信息记录了下来,他记录鼠标信息主要在后面touchend时候根据x、y判断是否为click
是ios情况下还搞了一些事情,我这里跳过去了
然后这里记录了一些事情就跳出去了,没有特别的事情,现在我们进入我们的出口touchend

 1 FastClick.prototype.onTouchEnd = function (event) {
 2     'use strict';
 3     var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement;
 4     log('touchend');
 5     if (!this.trackingClick) {
 6         return true;
 7     }
 8     if ((event.timeStamp - this.lastClickTime) < 200) {
 9         this.cancelNextClick = true;
10         return true;
11     }
12     this.lastClickTime = event.timeStamp;
13     trackingClickStart = this.trackingClickStart;
14     this.trackingClick = false;
15     this.trackingClickStart = 0;
16     if (this.deviceIsIOSWithBadTarget) {
17         touch = event.changedTouches[0];
18         targetElement = document.elementFromPoint(touch.pageX - window.pageXOffset, touch.pageY - window.pageYOffset) || targetElement;
19         targetElement.fastClickScrollParent = this.targetElement.fastClickScrollParent;
20     }
21     targetTagName = targetElement.tagName.toLowerCase();
22     if (targetTagName === 'label') {
23         forElement = this.findControl(targetElement);
24         if (forElement) {
25             this.focus(targetElement);
26             if (this.deviceIsAndroid) {
27                 return false;
28             }
29             targetElement = forElement;
30         }
31     } else if (this.needsFocus(targetElement)) {
32         if ((event.timeStamp - trackingClickStart) > 100 || (this.deviceIsIOS && window.top !== window && targetTagName === 'input')) {
33             this.targetElement = null;
34             return false;
35         }
36         this.focus(targetElement);
37         if (!this.deviceIsIOS4 || targetTagName !== 'select') {
38             this.targetElement = null;
39             event.preventDefault();
40         }
41         return false;
42     }
43     if (this.deviceIsIOS && !this.deviceIsIOS4) {
44         scrollParent = targetElement.fastClickScrollParent;
45         if (scrollParent && scrollParent.fastClickLastScrollTop !== scrollParent.scrollTop) {
46             return true;
47         }
48     }
49     if (!this.needsClick(targetElement)) {
50         event.preventDefault();
51         this.sendClick(targetElement, event);
52     }
53     return false;
54 };复制代码

这个家伙洋洋洒洒干了许多事情

这里纠正一个错误,他onclick那些东西现在也执行了......可能是我屏幕有变化(滑动)导致

1 if ((event.timeStamp - this.lastClickTime) < 200) {
2  this.cancelNextClick = true;
3  return true;
4 }复制代码

这个代码很关键,我们首次点击会执行下面的逻辑,如果连续点击就直接完蛋,下面的逻辑丫的不执行了......
这个不执行了,那么这个劳什子又干了什么事情呢?
事实上下面就没逻辑了,意思是如果确实点击过快,两次点击只会执行一次,这个阀值为200ms,这个暂时看来是没有问题的

好了,我们继续往下走,于是我意识到又到了一个关键点
因为我们用tap事件不能使input获得焦点,但是fastclick却能获得焦点,这里也许是一个关键,我们来看看几个与获取焦点有关的函数

 1 FastClick.prototype.focus = function (targetElement) {
 2     'use strict';
 3     var length;
 4     if (this.deviceIsIOS && targetElement.setSelectionRange) {
 5         length = targetElement.value.length;
 6         targetElement.setSelectionRange(length, length);
 7     } else {
 8         targetElement.focus();
 9     }
10 };复制代码

setSelectionRange是我们的关键,也许他是这样获取焦点的......具体我还要下来测试,留待下次处理吧
然后下面如果时间间隔过长,代码就不认为操作的是同一dom结构了
最后迎来了本次的关键:sendClick,无论是touchend还是onMouse都会汇聚到这里

 1 FastClick.prototype.sendClick = function (targetElement, event) {
 2     'use strict';
 3     var clickEvent, touch;
 4     // On some Android devices activeElement needs to be blurred otherwise the synthetic click will have no effect (#24)
 5     if (document.activeElement && document.activeElement !== targetElement) {
 6         document.activeElement.blur();
 7     }
 8     touch = event.changedTouches[0];
 9     // Synthesise a click event, with an extra attribute so it can be tracked
10     clickEvent = document.createEvent('MouseEvents');
11     clickEvent.initMouseEvent('click', true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);
12     clickEvent.forwardedTouchEvent = true;
13     targetElement.dispatchEvent(clickEvent);
14 };复制代码

他创建了一个鼠标事件,然后dispatchEvent事件(这个与fireEvent类似)

 1 //document上绑定自定义事件ondataavailable
 2 document.addEventListener('ondataavailable', function (event) {
 3 alert(event.eventType);
 4 }, false);
 5 var obj = document.getElementById("obj");
 6 //obj元素上绑定click事件
 7 obj.addEventListener('click', function (event) {
 8 alert(event.eventType);
 9 }, false);
10 //调用document对象的 createEvent 方法得到一个event的对象实例。
11 var event = document.createEvent('HTMLEvents');
12 // initEvent接受3个参数:
13 // 事件类型,是否冒泡,是否阻止浏览器的默认行为
14 event.initEvent("ondataavailable", true, true);
15 event.eventType = 'message';
16 //触发document上绑定的自定义事件ondataavailable
17 document.dispatchEvent(event);
18 
19 var event1 = document.createEvent('HTMLEvents');
20 event1.initEvent("click", true, true);
21 event1.eventType = 'message';
22 //触发obj元素上绑定click事件
23 document.getElementById("test").onclick = function () {
24 obj.dispatchEvent(event1);
25 };复制代码

至此,我们就知道了,我们为dom先绑定了鼠标事件,然后touchend时候触发了,而至于为什么本身注册的click未触发就要回到上面代码了

解决“点透”(成果)

有了这个思路,我们来试试我们抽象出来的代码:

 1 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 2 <html xmlns="http://www.w3.org/1999/xhtml">
 3 <head>
 4     <title></title>
 5     <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
 6     <style>
 7         #list { display: block; position: absolute; top: 100px; left: 10px; width: 200px; height: 100px; }
 8         div { display: block; border: 1px solid black; height: 300px; width: 100%; }
 9         #input { width: 80px; height: 200px; display: block; }
10     </style>
11 </head>
12 <body>
13     <div id="list" style="background: gray;">
14     </div>
15     <div id="wrapper">
16         <div id="d">
17             <input type="text" id="input" />
18         </div>
19     </div>
20     <script type="text/javascript">
21         var el = null;
22         function getEvent(el, e, type) {
23             e = e.changedTouches[0];
24             var event = document.createEvent('MouseEvents');
25             event.initMouseEvent(type, true, true, window, 1, e.screenX, e.screenY, e.clientX, e.clientY, false, false, false, false, 0, null);
26             event.forwardedTouchEvent = true;
27             return event;
28         }
29         list.addEventListener('touchstart', function (e) {
30             var firstTouch = e.touches[0]
31             el = firstTouch.target;
32             t1 = e.timeStamp;
33         })
34         list.addEventListener('touchend', function (e) {
35             e.preventDefault();
36             var event = getEvent(el, e, 'click');
37             el.dispatchEvent(event);
38         })
39         var list = document.getElementById('list');
40         list.addEventListener('click', function (e) {
41             list.style.display = 'none';
42             setTimeout(function () {
43                 list.style.display = '';
44             }, 1000);
45         })
46     </script>
47 </body>
48 </html>复制代码

这样的话,便不会点透了,这是因为zepto touch事件全部绑定值document,所以 e.preventDefault();无用
结果我们这里是直接在dom上,e.preventDefault();
便起了作用不会触发浏览器默认事件,所以也不存在点透问题了,至此点透事件告一段落......

帮助理解的图

代码在公司写的,回家后不知道图上哪里了,各位将就看吧

为什么zepto会点透/fastclick如何解决点透

我最开始就给老大说zepto处理tap事件不够好,搞了很多事情出来

因为他事件是绑定到document上,先touchstart然后touchend,根据touchstart的event参数判断该dom是否注册了tap事件,有就触发

于是问题来了,zepto的touchend这里有个event参数,我们event.preventDefault(),这里本来都是最上层了,这就代码压根没什么用

但是fastclick处理办法不可谓不巧妙,这个库直接在touchend的时候就触发了dom上的click事件而替换了本来的触发时间

意思是原来要350-400ms执行的代码突然就移到了50-100ms,然后这里虽然使用了touch事件但是touch事件是绑定到了具体dom而不是document上

所以e.preventDefault是有效的,我们可以阻止冒泡,也可以阻止浏览器默认事件,这个才是fastclick的精华部分,不可谓不高啊!!!

整个fastclick代码读来醍醐灌顶,今天收获很大,在此记录

后记

上面的说法有点问题,这修正一下:

首先,我们回到原来的zepto方案,看看他有什么问题:

  1. 因为js标准本不支持tap事件,所以zepto tap是touchstart与touchend模拟而出
  2. zepto在初始化时便给document绑定touch事件,在我们点击时根据event参数获得当前元素,并会保存点下和离开时候的鼠标位置
  3. 根据当前元素鼠标移动范围判断是否为类点击事件,如果是便触发已经注册好的tap事件

然后fastclick处理比较与zepto基本一致,但是又有所不同

  1. fastclick是将事件绑定到你传的元素(一般是document.body)

② 在touchstart和touchend后(会手动获取当前点击el),如果是类click事件便手动触发了dom元素的click事件

所以click事件在touchend便被触发,整个响应速度就起来了,触发实际与zepto tap一样

好了,为什么基本相同的代码,zepto会点透而fastclick不会呢?

原因是zepto的代码里面有个settimeout,而就算在这个代码里面执行e.preventDefault()也不会有用

这就是根本区别,因为settimeout会将优先级较低

有了定期器,当代码执行到setTimeout的时候, 就会把这个代码放到JS的引擎的最后面

而我们代码会马上检测到e.preventDefault,一旦加入settimeout,e.preventDefault便不会生效,这是zepto点透的根本原因

结语

虽然,这次走了很多弯路,但是最后终于解决了问题




其实有好多文章都写了,内容有很多我就不重复,总结以下几点:

  1. 300ms延迟是由于浏览器要判断是单机还是双击造成的延迟处理点击事件

  2. fastclick解决方式用touchstart结合touchmove以及touchend替代click事件

  3. zepto的tap会“击穿”页面是由于既响应了自身的tap(也就是touch事件),又没有拦截掉原来的click事件,导致重复执行了2次事件,在有遮罩弹层的时候就会出现“击穿”效果。如果不太明白的话看这篇文章zepto的击穿

年前探究

当时研究到这里时候我有一个大大的疑问就是为什么click延迟执行之后,遮罩层下面的页面的click事件会被触发,我明明点击的遮罩层的A按钮,为何下面页面的B按钮的事件会执行。按照我最初的想法,应该是继续执行A按钮的事件啊!!!此时我内心是这样的


于是我开始探究这个问题,我搜了下大概的资料,基本都没有讲这个具体原因的,也许是我打开方式不对,反正没有找到,无奈之下,我只能翻看fastclick的源码来看它为何没有出现这个问题,然后看到了sendClick的代码,心里猛然有了一个猜想。

FastClick.prototype.sendClick = function(targetElement, event) {
    var clickEvent, touch;
    // On some Android devices activeElement needs to be blurred otherwise the synthetic click will have no effect (#24)
    if (document.activeElement && document.activeElement !== targetElement) {
        document.activeElement.blur();
    }
    touch = event.changedTouches[0];
    // Synthesise a click event, with an extra attribute so it can be tracked
    clickEvent = document.createEvent('MouseEvents');
    clickEvent.initMouseEvent(this.determineEventType(targetElement), true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);
    clickEvent.forwardedTouchEvent = true;
    targetElement.dispatchEvent(clickEvent);
    };复制代码

注意这里的initMouseEvent,当时就在想肯定和mouseEvent执行的原理有关了,到这个阶段算是有了眉目。

接着搞

紧接着,开始过年,过年期间享受了生活,并没有碰代码和文档(好堕落的感觉......),加上我跳槽的空档和折腾,年后稍稍稳定下来了,最近又想起了年前这探究一半的猜想,开始继续搞了起来,顺便收收心,好进入状态。

先说猜想--click事件最开始其实在浏览器当中被捕捉的时候,只有mouseEvent的相关属性,也就是我们平常在console.log(event)的一部分,之后,浏览器才会结合html,js产生我们常说的click时间,接着触发我们使用js绑定的函数。

一般情况的event的各种属性

基于这个猜想,我开始翻阅mozillaW3C的文档来了解mouseEvent。

翻看文档之后发现mouseEvent果然只有 screenX,screenY,clientX,clientY,ctrlKey,altKey,shiftKey,metaKey,button,buttons,EventTarget?relatedTarget。

其中button和buttons指的是鼠标的按钮类型,就是左键,右键,滚轮这些。用数字代替,0表示左键,1是滚轮,2是右键,其他更多功能键,都是大于2的。

从上面我们能看出来,其实对于mouseEvent而言,它只知道我们在屏幕的哪个位置,做了什么动作(鼠标操作),并不知道是在哪个element上面。这也就是fastclick还原用户点击事件最后做的事情。

clickEvent.initMouseEvent(this.determineEventType(targetElement), true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);
// detremineEvenType是fastclick封装返回mouseEvent的type类型,就是click还是mouseDown复制代码

初始化一个鼠标事件,然后dispatch这个鼠标事件。浏览器自动响应后续处理。

接着来看click的定义,如下图所示:

click的属性


click会多了Event.target,而且必须是一个 topmost event target,在mozilla定义有些不太相同,多了currentTarget和type等。

mozilla的click

先来看EventTarget的定义:EventTarget is an interface implemented by objects that can receive events and may have listeners for them.

Element, document, and window are the most common event targets, but other objects can be event targets too, for example XMLHttpRequest, AudioNode,AudioContext, and others.

从定义就能看出来了,如果是click事件必须要有一个target来承载这次鼠标事件。一般来说target要么是element要么是document,如果都没有那么就是window对象了。到这里大家应该就比较明白,这里就是浏览器的事件机制了。

event-flow

这里就应该是initMouseEvent之后,浏览器干的事情,来寻找是否有target来响应此次事件,如果前面一直没有target来响应,最后就会到window上,一般来说我们不会在window上做事件处理,就会没有任何响应,事件结束了。如果碰巧的事,此时有target(一般来说就是element了)来响应,那么就会执行绑定的函数了。

总结下整个流程:用户点击屏幕,300ms之内,浏览器拦截下这个行为,没有去真正触发相关element上绑定的click事件执行函数,而是记录操作相关数据,等待接下来的操作,由于我们使用zepto库绑定了tap事件,事件中有监听touchend触发了,立刻执行相关操作,隐藏了弹层。300ms到了,浏览器认为这次动作是click而不是dbclick,然后init一次mouseEvent在相同的屏幕位置,接着开始事件机制,发现相同位置有一个element绑定了click处理函数,执行这个函数,Over!!!穿透就是这样产生的。PS:浏览器行为部分是猜测,未验证。

至于解决方案:网上有很多,目前最好的是fastclick,不过fastclick也会有其他问题,例如在滑动中点击之类的。另外就是用zepto但是要preventDefault。

Android自己chrome已经解决了,可以用其他方式,官方文档,目前Safari也支持了,不过是在高版本上,相关讨论可以看fastclick的issue



众所周知,移动端在处理点击事件的时候,会有300毫秒的延迟。恰恰是这300毫秒的延迟,会让人有一种卡顿的体验。

这300毫秒的原因,在于早期浏览器的实现中,浏览器不知道用户触摸后,到底想做什么,所以故意等待300毫秒,再触发click事件。

既然我们已经知道了原因了,怎么解决呢?

方案1-粗暴治标法

因为浏览器对click事件的处理,有300ms的延迟,而touchstart几乎是立即执行的,估将所有click事件的监听,改为touchstart事件的监听,即可消除这300ms的延迟。

但这样副作用也很大,移动端的交互体验全靠触摸,touchstart将会干扰其他交互行为的处理,例如滚动、拖拽等。

方案2-模拟修复法

既然浏览器有这300ms的延迟,那么我们来代替浏览器判断,手动触发click事件,这也是fastClick的解决方案。

fastClick的核心代码

FastClick.prototype.onTouchEnd = function(event){

  // 一些状态监测代码 

  // 从这里开始,
  if (!this.needsClick(targetElement)) {
    // 如果这不是一个需要使用原生click的元素,则屏蔽原生事件,避免触发两次click
    event.preventDefault(); 
    // 触发一次模拟的click
    this.sendClick(targetElement, event);
  }
}复制代码

这里可以看到,FastClick在touchEnd的时候,在符合条件的情况下,主动触发了click事件,这样避免了浏览器默认的300毫秒等待判断。为了防止原生的click被触发,这里还通过event.preventDefault()屏蔽了原生的click事件。

我们来看看他是怎么模拟click事件的

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);复制代码

我们在网上搜索fastClick,大部分都在说他解决了zepto的点击穿透问题,他是怎么解决的呢?就是上面最后一句,他模拟的click事件是在touchEnd获取的真实元素上触发的,而不是通过坐标计算出来的元素。

最后,原理虽简单,但还是建议大家直接用FastClick而不是自己再实现一个。因为,你看他源码里面的注释,有很多特殊情况的补丁的,自己实现一个精简版难免会漏这漏那。





做过移动端H5页面的同学肯定知道,移动端web的事件模型不同于PC页面的事件。看了一些关于touch事件的文章,我想再来回顾下touch事件的原理,为什么通过touch可以触发click事件,touch事件是不是万能的以及它可能存在的问题。

touch事件的来源

PC网页上的大部分操作都是用鼠标的,即响应的是鼠标事件,包括mousedownmouseupmousemoveclick事件。一次点击行为,事件的触发过程为:mousedown -> mouseup -> click 三步。

手机上没有鼠标,所以就用触摸事件去实现类似的功能。touch事件包含touchstarttouchmovetouchend,注意手机上并没有tap事件。手指触发触摸事件的过程为:touchstart -> touchmove -> touchend

手机上没有鼠标,但不代表手机不能响应mouse事件(其实是借助touch去触发mouse事件)。有人在PC和手机上对事件做了对比实验,以说明手机对touch事件相应速度快于mouse事件。

可以看到在手机上,当我们手触碰屏幕时,要过300ms左右才会触发mousedown事件,所以click事件在手机上看起来就像慢半拍一样。

touch事件中可以获取以下参数

参数含义
touches屏幕中每根手指信息列表
targetTouches和touches类似,把同一节点的手指信息过滤掉
changedTouches响应当前事件的每根手指的信息列表

tap是怎么来的

用过Zepto或KISSY等移动端js库的人肯定对tap事件不陌生,我们做PC页面时绑定click,相应地手机页面就绑定tap。但原生的touch事件本身是没有tap的,js库里提供的tap事件都是模拟出来的。

我们在上面看到,手机上响应 click 事件会有300ms的延迟,那么这300ms到底是干嘛了?浏览器在 touchend 后会等待约300ms,原因是判断用户是否有双击(double tap)行为。如果没有 tap 行为,则触发 click 事件,而双击过程中就不适合触发 click 事件了。由此可以看出 click 事件触发代表一轮触摸事件的结束。

既然说tap事件是模拟出来的,我们可以看下Zepto对 singleTap 事件的处理。见源码 136-143 行,可以看出在 touchend 响应 250ms 无操作后,则触发singleTap。

点击穿透的场景

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

整个容器里有一个底层元素的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('tap', function(e){
    $('#popupLayer').hide();
    $('#bgMask').hide();
});

$('#underLayer').on('click', function(){
    alert('underLayer clicked');
});
复制代码

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

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

完整demo请用chrome手机模拟器查看,或直接扫描二维码在手机上查看。

结合Zepto源码的解释

zepto中的 tap 通过兼听绑定在 document 上的 touch 事件来完成 tap 事件的模拟的,是通过事件冒泡实现的。在点击完成时(touchstart / touchend)的 tap 事件需要冒泡到 document 上才会触发。而在冒泡到 document 之前,手指接触和离开屏幕(touchstart / touchend)是会触发 click 事件的。

因为 click 事件有延迟(大概是300ms,为了实现safari的双击事件的设计),所以在执行完 tap 事件之后,弹出层立马就隐藏了,此时 click 事件还在延迟的 300ms 之中。当 300ms 到来的时候,click 到的其实是隐藏元素下方的元素。

如果正下方的元素有绑定 click 事件,此时便会触发,如果没有绑定 click 事件的话就当没发生。如果正下方的是 input 输入框(或是 select / radio / checkbox),点击默认 focus 而弹出输入键盘,也就出现了上面的“点透”现象。

穿透的解决办法

1. 遮挡

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

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

2. pointer-events

pointer-events是CSS3中的属性,它有很多取值,有用的主要是autonone,其他属性值为SVG服务。

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

关于使用 pointer-events 后的事件冒泡,有人做了个实验,见代码

因此解决“穿透”的办法就很简单,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 事件(参看源码 164-173 行),用 touchend 模拟快速点击行为(参看源码 521-610 行)。

FastClick.attach(document.body);
复制代码

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

有人(叶小钗)对事件机制做了详细的剖析,循循善诱,并剖析了fastclick的源码以自己模拟事件的创建。请看这篇文章,看完后一定会对移动端的事件有更深的了解


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值