问题1:300ms延迟问题指的是?

不管在移动端还是PC端,我们都需要处理用户点击,这个最常用的事件。但在touch端click事件响应速度会比较慢,在较老的手机设备上会更为明显(300ms的延迟)。双击缩放(double tap to zoom),这也是会有上述 300 毫秒延迟的主要原因。双击缩放,顾名思义,即用手指在屏幕上快速点击两次,iOS 自带的 Safari 浏览器会将网页缩放至原始比例。 
假定这么一个场景。用户在 iOS Safari 里边点击了一个链接。由于用户可以进行双击缩放或者双击滚动的操作,当用户一次点击屏幕之后,浏览器并不能立刻判断用户是确实要打开这个链接,还是想要进行双击操作。因此,iOS Safari 就等待 300 毫秒,以判断用户是否再次点击了屏幕。鉴于iPhone的成功,其他移动浏览器都复制了 iPhone Safari 浏览器的多数约定,包括双击缩放,几乎现在所有的移动端浏览器都有这个功能。之前人们刚刚接触移动端的页面,在欣喜的时候往往不会care这个300ms的延时问题,可是如今touch端界面如雨后春笋,用户对体验的要求也更高,这300ms带来的卡顿慢慢变得让人难以接受。
那么我们该如何解决这个问题,可能有的同学会想到touchstart事件,这个事件响应速度很快啊,如果说开发的界面上面可点击的地方很少,要么用户滑动下手指就触touchstart事件,也会让人崩溃的
问题2:300ms延迟的解决方案?

在参考文献1中列出了三种方法。

第一种方法:

<meta name="viewport" content="user-scalable=no"/>
<meta name="viewport" content="initial-scale=1,maximum-scale=1"/>

这个方案有一个缺点,就是必须通过完全禁用缩放来达到去掉点击延迟的目的,然而完全禁用缩放并不是我们的初衷,我们只是想禁掉默认的双击缩放行为,这样就不用等待300ms来判断当前操作是否是双击。但是通常情况下,我们还是希望页面能通过 双指缩放来进行缩放操作,比如放大一张图片,放大一段很小的文字。
第二种方法:

touch-action:none

跟300ms点击延迟相关的,是touch-action这个CSS属性。这个属性指定了相应元素上能够触发的用户代理(也就是浏览器)的默认行为。如果将该属性值设置为touch-action: none,那么表示在该元素上的操作不会触发用户代理的任何默认行为,就无需进行300ms的延迟判断。

第三种方法:

也就是fastClick解决300ms延迟的问题。

问题3:分析fastClick之前,我们看看zepto的touch.js?

解答:分析fastClick.js之前,我们先对zepto的touch.js进行的分析:

touch.js为每一个实例对象注册了swipe, tap等事件:

['swipe', 'swipeLeft', 'swipeRight', 'swipeUp', 'swipeDown','doubleTap', 'tap', 'singleTap', 'longTap'].forEach(function(eventName){//调用的时候只要传入我们的回调函数就可以了,这一点要注意一下!那么内部会判断具体的事件类型,可以是如下方式的://'swipe', 'swipeLeft', 'swipeRight', 'swipeUp', 'swipeDown','doubleTap', 'tap', 'singleTap', 'longTap'$.fn[eventName] = function(callback){ return this.on(eventName, callback) }})
})(Zepto)

也就是说可以通过下面的方式为实例注册事件:

$('body').tap(function(){console.log('taped');});

如何判断是否是pointer事件:

 //是否是指针事件类型,事件类型为'pointer'+type||'mspointer'+typefunction isPointerEventType(e, type){return (e.type == 'pointer'+type ||e.type.toLowerCase() == 'mspointer'+type)} 

如何判断是否是主触点:

//@isPrimaryTouch表示是touch
//表示事件是来自于手指还是手写笔还是鼠标
//MSPOINTER_TYPE_TOUCH手指;MSPOINTER_TYPE_PEN手写笔;MSPOINTER_TYPE_MOUSE鼠标function isPrimaryTouch(event){//必须来自于手指,因为zepto是针对touch来说return (event.pointerType == 'touch' ||event.pointerType == event.MSPOINTER_TYPE_TOUCH)//pointerType:一个整数,标识了该事件来自鼠标、手写笔还是手指&& event.isPrimary}

判断滑动的方向:

//@swipeDirection首先根据x和y的变换长度来决定是触发x还是y轴的移动,然后再决定是做滑动还是右滑动function swipeDirection(x1, x2, y1, y2) {return Math.abs(x1 - x2) >=Math.abs(y1 - y2) ? (x1 - x2 > 0 ? 'Left' : 'Right') : (y1 - y2 > 0 ? 'Up' : 'Down')}

因为touch.js主要是对触屏事件进行分析,所以这里主要判断的是手指touch事件。接下来我们看看在domReady后touch.js主要做了什么:

 //表示DOMContentLoaded事件$(document).ready(function(){var now, delta, deltaX = 0, deltaY = 0, firstTouch, _isPointerType//window是否存在MSGesture对象。MSGesture提供了一些方法和属性代表页面的一系列交互,如touch,mouse,pen等。详见IE浏览器https://msdn.microsoft.com/en-us/library/windows/apps/hh968035.aspxif ('MSGesture' in window) {gesture = new MSGesture()gesture.target = document.body//target表示:你想要触发MSGestureEvents的Element对象}$(document)//手势完全被处理的时候触发.bind('MSGestureEnd', function(e){var swipeDirectionFromVelocity =//velocityX,velocityY用于判断元素的移动方向 e.velocityX > 1 ? 'Right' : e.velocityX < -1 ? 'Left' : e.velocityY > 1 ? 'Down' : e.velocityY < -1 ? 'Up' : null;if (swipeDirectionFromVelocity) {//触发swipe事件touch.el.trigger('swipe')touch.el.trigger('swipe'+ swipeDirectionFromVelocity)}})//触摸开始touchstart,MSPointerDown,pointerdown.on('touchstart MSPointerDown pointerdown', function(e){if((_isPointerType = isPointerEventType(e, 'down')) &&!isPrimaryTouch(e)) return//如果是往下移动,但是不是isPrimaryTouch那么我们不作处理//http://www.w3cplus.com/css3/adapting-your-webkit-optimized-site-for-internet-explorer-10.htmlfirstTouch = _isPointerType ? e : e.touches[0]//touches:当前位于屏幕上的所有手指的列表。if (e.touches && e.touches.length === 1 && touch.x2) {// Clear out touch movement data if we have it sticking around// This can occur if touchcancel doesn't fire due to preventDefault, etc.//清除touchmove的数据,一般当touchcancel没有触发的时候调用(例如,preventDefault)touch.x2 = undefinedtouch.y2 = undefined}now = Date.now()delta = now - (touch.last || now)touch.el = $('tagName' in firstTouch.target ?firstTouch.target : firstTouch.target.parentNode)//触摸的事件的target表示当前的Element对象,如果当前对象不是Element对象,那么就获取parentNode对象touchTimeout && clearTimeout(touchTimeout)touch.x1 = firstTouch.pageXtouch.y1 = firstTouch.pageY//x1,y1存储的是pageX,pageY属性if (delta > 0 && delta <= 250) touch.isDoubleTap = true//如果两次触屏在[0,250]表示双击touch.last = nowlongTapTimeout = setTimeout(longTap, longTapDelay)//这里是长按// adds the current touch contact for IE gesture recognitionif (gesture && _isPointerType) gesture.addPointer(e.pointerId);//如果是IE浏览器,为new MSGesture()对象添加一个Pointer对象,传入的是我们的event对象的pointerId的值//把元素上的一个触点添加到MSGesture对象上!}).on('touchmove MSPointerMove pointermove', function(e){if((_isPointerType = isPointerEventType(e, 'move')) &&!isPrimaryTouch(e)) returnfirstTouch = _isPointerType ? e : e.touches[0]//这时候我们取消长按的事件处理程序,因为这里是pointermove,也就是手势移动了那么肯定不会是长按了//但是因为我们在pointerdown中不知道是否是长按还是pointermove,所以才默认使用的长按。如果移动了,那么就知道不是长按了cancelLongTap()touch.x2 = firstTouch.pageXtouch.y2 = firstTouch.pageYdeltaX += Math.abs(touch.x1 - touch.x2)deltaY += Math.abs(touch.y1 - touch.y2)//得到两者在X和Y方向移动的绝对距离}).on('touchend MSPointerUp pointerup', function(e){if((_isPointerType = isPointerEventType(e, 'up')) &&!isPrimaryTouch(e)) return//触摸结束,这时候我们取消掉长按的定时器,因为我们也已经知道不是长按了,所以取消长按的定时器cancelLongTap()// swipeif ((touch.x2 && Math.abs(touch.x1 - touch.x2) > 30) ||(touch.y2 && Math.abs(touch.y1 - touch.y2) > 30))//如果任意一方向移动的距离大于30,那么表示触发swipe事件。触发了swipe事件后,我们清除touch列表,也就是设置为touch={}//因为移动结束了,那么必须重置为空swipeTimeout = setTimeout(function() {touch.el.trigger('swipe')touch.el.trigger('swipe' + (swipeDirection(touch.x1, touch.x2, touch.y1, touch.y2)))touch = {}}, 0)// normal tap//这里是正常的tap事件,last表示上一次点击的时间,也就是说已经发生了点击了//last属性是在touchstart中进行赋值的,因此如果存在那么表示已经点击过了//但是不管是双击还是单击都会运行到这里的代码!!!else if ('last' in touch)// don't fire tap when delta position changed by more than 30 pixels,// for instance when moving to a point and back to origin//如果移动距离超过30那么我们不会触发tap事件,因为触摸事件不是移动,不能让他移动了30px了if (deltaX < 30 && deltaY < 30) {// delay by one tick so we can cancel the 'tap' event if 'scroll' fires// ('tap' fires before 'scroll')//tap事件是在scroll之前,所以如果scroll了那么我们取消tap事件tapTimeout = setTimeout(function() {// trigger universal 'tap' with the option to cancelTouch()// (cancelTouch cancels processing of single vs double taps for faster 'tap' response)var event = $.Event('tap')event.cancelTouch = cancelAlltouch.el.trigger(event)//tap会立即触发// trigger double tap immediately//如果是双击,那么我们立即触发doubleTap事件,这时候我们的touch={}那么后面的singleTap是不会执行的if (touch.isDoubleTap) {if (touch.el) touch.el.trigger('doubleTap')touch = {}}// trigger single tap after 250ms of inactivity//如果不是双击,那么此时tap已经触发了,等待250ms我们再次触发singleTap事件else {touchTimeout = setTimeout(function(){touchTimeout = nullif (touch.el) touch.el.trigger('singleTap')touch = {}//情况touch对象,因为手指已经离开屏幕了}, 250)}}, 0)} else {touch = {}}//注意:不管是singleTap还是doubleTap,swipe,调用后都会清空touch={},也就是这时候是重新开始的触摸事件了deltaX = deltaY = 0})// when the browser window loses focus,// for example when a modal dialog is shown,// cancel all ongoing events//触摸被取消(触摸被一些事情中断,比如通知).on('touchcancel MSPointerCancel pointercancel', cancelAll)// scrolling the window indicates intention of the user// to scroll, not tap or swipe, so cancel all ongoing events$(window).on('scroll', cancelAll)})

第一步:我们首先看看对于IE浏览器的触屏事件的处理:

//window是否存在MSGesture对象。MSGesture提供了一些方法和属性代表页面的一系列交互,如touch,mouse,pen等。
详见IE浏览器https://msdn.microsoft.com/en-us/library/windows/apps/hh968035.aspxif ('MSGesture' in window) {gesture = new MSGesture()gesture.target = document.body//target表示:你想要触发MSGestureEvents的Element对象}

然后在pointerdown和MSPointerdown中为MSGesture对象添加要跟踪的触点,这样我们的gesture.target就可以响应相应的事件了。

        if (gesture && _isPointerType) gesture.addPointer(e.pointerId);//如果是IE浏览器,为new MSGesture()对象添加一个Pointer对象,传入的是我们的event对象的pointerId的值//把元素上的一个触点添加到MSGesture对象上!

同时MSGestureEnd也进行了绑定:

$(document)//手势完全被处理的时候触发.bind('MSGestureEnd', function(e){var swipeDirectionFromVelocity =//velocityX,velocityY用于判断元素的移动方向 e.velocityX > 1 ? 'Right' : e.velocityX < -1 ? 'Left' : e.velocityY > 1 ? 'Down' : e.velocityY < -1 ? 'Up' : null;if (swipeDirectionFromVelocity) {//触发swipe事件touch.el.trigger('swipe')touch.el.trigger('swipe'+ swipeDirectionFromVelocity)}})

其通过event对象的 velocityX,velocityY属性来判断手势的方向
第二步:下面我们主要分析一下浏览器的touchstart,MSPointerDown pointerdown事件绑定

//触摸开始touchstart,MSPointerDown,pointerdown.on('touchstart MSPointerDown pointerdown', function(e){if((_isPointerType = isPointerEventType(e, 'down')) &&!isPrimaryTouch(e)) return//如果是往下移动,但是不是isPrimaryTouch那么我们不作处理//http://www.w3cplus.com/css3/adapting-your-webkit-optimized-site-for-internet-explorer-10.htmlfirstTouch = _isPointerType ? e : e.touches[0]//touches:当前位于屏幕上的所有手指的列表。if (e.touches && e.touches.length === 1 && touch.x2) {// Clear out touch movement data if we have it sticking around// This can occur if touchcancel doesn't fire due to preventDefault, etc.//清除touchmove的数据,一般当touchcancel没有触发的时候调用(例如,preventDefault)touch.x2 = undefinedtouch.y2 = undefined}now = Date.now()delta = now - (touch.last || now)touch.el = $('tagName' in firstTouch.target ?firstTouch.target : firstTouch.target.parentNode)//触摸的事件的target表示当前的Element对象,如果当前对象不是Element对象,那么就获取parentNode对象touchTimeout && clearTimeout(touchTimeout)touch.x1 = firstTouch.pageXtouch.y1 = firstTouch.pageY//x1,y1存储的是pageX,pageY属性if (delta > 0 && delta <= 250) touch.isDoubleTap = true//如果两次触屏在[0,250]表示双击touch.last = nowlongTapTimeout = setTimeout(longTap, longTapDelay)//这里是长按// adds the current touch contact for IE gesture recognitionif (gesture && _isPointerType) gesture.addPointer(e.pointerId);//如果是IE浏览器,为new MSGesture()对象添加一个Pointer对象,传入的是我们的event对象的pointerId的值//把元素上的一个触点添加到MSGesture对象上!})

首先,对于两次tap而言,如果相隔的时间小于250ms就会被认为是'doubleTap':

        if (delta > 0 && delta <= 250) touch.isDoubleTap = true//相隔小于250ms

然后,对于任何一次触摸事件都会首先假设是长按,也就是"longTap",并注册longTap回调函数,如果在指定的时间内 发生了move或者up类事件就清除回调:

function longTap() {longTapTimeout = null//touch.last表示是上一次触屏的时间,我们触发了longTap事件,longTap事件触发了以后我们清空touch对象为{}//之所以要清空是因为,长按后不需要马上跟踪touchstart等if (touch.last) {touch.el.trigger('longTap')touch = {}}}

下面是假设为长按事件,然后添加的回调函数:

        longTapTimeout = setTimeout(longTap, longTapDelay)

最后,还要记录手指放下时候的坐标:

        touch.x1 = firstTouch.pageXtouch.y1 = firstTouch.pageY

第三步:我们看看 touchmove MSPointerMove pointermove等事件

.on('touchmove MSPointerMove pointermove', function(e){if((_isPointerType = isPointerEventType(e, 'move')) &&!isPrimaryTouch(e)) returnfirstTouch = _isPointerType ? e : e.touches[0]//这时候我们取消长按的事件处理程序,因为这里是pointermove,也就是手势移动了那么肯定不会是长按了//但是因为我们在pointerdown中不知道是否是长按还是pointermove,所以才默认使用的长按。如果移动了,那么就知道不是长按了cancelLongTap()touch.x2 = firstTouch.pageXtouch.y2 = firstTouch.pageYdeltaX += Math.abs(touch.x1 - touch.x2)deltaY += Math.abs(touch.y1 - touch.y2)//得到两者在X和Y方向移动的绝对距离})

首先:因为触点已经移动,所以我们要取消一开始为长按的假设

   cancelLongTap()

然后:判断触点移动的终点位置,然后记录触点变换的距离大小

       touch.x2 = firstTouch.pageXtouch.y2 = firstTouch.pageY//x2,y2为终点deltaX += Math.abs(touch.x1 - touch.x2)deltaY += Math.abs(touch.y1 - touch.y2)//deltaX,deltaY表示移动的变化量

第四步:我们看看touchend MSPointerUp pointerup事件

注意:这个事件是touch.js中最重要的事件,因为他要用于判断tap,doubleTap,singleTap等事件类型

 .on('touchend MSPointerUp pointerup', function(e){if((_isPointerType = isPointerEventType(e, 'up')) &&!isPrimaryTouch(e)) return//触摸结束,这时候我们取消掉长按的定时器,因为我们也已经知道不是长按了,所以取消长按的定时器cancelLongTap()// swipeif ((touch.x2 && Math.abs(touch.x1 - touch.x2) > 30) ||(touch.y2 && Math.abs(touch.y1 - touch.y2) > 30))//如果任意一方向移动的距离大于30,那么表示触发swipe事件。触发了swipe事件后,我们清除touch列表,也就是设置为touch={}//因为移动结束了,那么必须重置为空swipeTimeout = setTimeout(function() {touch.el.trigger('swipe')touch.el.trigger('swipe' + (swipeDirection(touch.x1, touch.x2, touch.y1, touch.y2)))touch = {}}, 0)// normal tap//这里是正常的tap事件,last表示上一次点击的时间,也就是说已经发生了点击了//last属性是在touchstart中进行赋值的,因此如果存在那么表示已经点击过了//但是不管是双击还是单击都会运行到这里的代码!!!else if ('last' in touch)// don't fire tap when delta position changed by more than 30 pixels,// for instance when moving to a point and back to origin//如果移动距离超过30那么我们不会触发tap事件,因为触摸事件不是移动,不能让他移动了30px了if (deltaX < 30 && deltaY < 30) {// delay by one tick so we can cancel the 'tap' event if 'scroll' fires// ('tap' fires before 'scroll')//tap事件是在scroll之前,所以如果scroll了那么我们取消tap事件tapTimeout = setTimeout(function() {// trigger universal 'tap' with the option to cancelTouch()// (cancelTouch cancels processing of single vs double taps for faster 'tap' response)var event = $.Event('tap')event.cancelTouch = cancelAlltouch.el.trigger(event)//tap会立即触发// trigger double tap immediately//如果是双击,那么我们立即触发doubleTap事件,这时候我们的touch={}那么后面的singleTap是不会执行的if (touch.isDoubleTap) {if (touch.el) touch.el.trigger('doubleTap')touch = {}}// trigger single tap after 250ms of inactivity//如果不是双击,那么此时tap已经触发了,等待250ms我们再次触发singleTap事件else {touchTimeout = setTimeout(function(){touchTimeout = nullif (touch.el) touch.el.trigger('singleTap')touch = {}//情况touch对象,因为手指已经离开屏幕了}, 250)}}, 0)} else {touch = {}}//注意:不管是singleTap还是doubleTap,swipe,调用后都会清空touch={},也就是这时候是重新开始的触摸事件了deltaX = deltaY = 0})

首先,因为触点已经离开界面,所以前面假设是长按的假设不成立:

 cancelLongTap()//不是长按

然后,如果触点变换的距离大于30,那么触发 swipe,swipeDown等类型

  if ((touch.x2 && Math.abs(touch.x1 - touch.x2) > 30) ||(touch.y2 && Math.abs(touch.y1 - touch.y2) > 30))//如果任意一方向移动的距离大于30,那么表示触发swipe事件。触发了swipe事件后,我们清除touch列表,也就是设置为touch={}//因为移动结束了,那么必须重置为空swipeTimeout = setTimeout(function() {touch.el.trigger('swipe')touch.el.trigger('swipe' + (swipeDirection(touch.x1, touch.x2, touch.y1, touch.y2)))touch = {}}, 0)

最后,tap,doubleTap,singleTap都是有一定的触发条件的

 //这里是正常的tap事件,last表示上一次点击的时间,也就是说已经发生了点击了//last属性是在touchstart中进行赋值的,因此如果存在那么表示已经点击过了//但是不管是双击还是单击都会运行到这里的代码!!!else if ('last' in touch)// don't fire tap when delta position changed by more than 30 pixels,// for instance when moving to a point and back to origin//如果移动距离超过30那么我们不会触发tap事件,因为触摸事件不是移动,不能让他移动了30px了if (deltaX < 30 && deltaY < 30) {// delay by one tick so we can cancel the 'tap' event if 'scroll' fires// ('tap' fires before 'scroll')//tap事件是在scroll之前,所以如果scroll了那么我们取消tap事件tapTimeout = setTimeout(function() {// trigger universal 'tap' with the option to cancelTouch()// (cancelTouch cancels processing of single vs double taps for faster 'tap' response)var event = $.Event('tap')event.cancelTouch = cancelAlltouch.el.trigger(event)//tap会立即触发// trigger double tap immediately//如果是双击,那么我们立即触发doubleTap事件,这时候我们的touch={}那么后面的singleTap是不会执行的if (touch.isDoubleTap) {if (touch.el) touch.el.trigger('doubleTap')touch = {}}// trigger single tap after 250ms of inactivity//如果不是双击,那么此时tap已经触发了,等待250ms我们再次触发singleTap事件else {touchTimeout = setTimeout(function(){touchTimeout = nullif (touch.el) touch.el.trigger('singleTap')touch = {}//情况touch对象,因为手指已经离开屏幕了}, 250)}}, 0)} else {touch = {}}//注意:不管是singleTap还是doubleTap,swipe,调用后都会清空touch={},也就是这时候是重新开始的触摸事件了deltaX = deltaY = 0})

我们从上面的代码可以看出tap,doubleTap,singleTap的区别是什么

(1)代码会通过等待250ms判断是否是双击,如果是双击那么就会触发doubleTap,否则250ms后会触发singleTap。

          else {touchTimeout = setTimeout(function(){touchTimeout = nullif (touch.el) touch.el.trigger('singleTap')touch = {}//250ms后如果没有继续触摸,那么触发singleTap}, 250)}

如果在250ms中继续触摸了屏幕,那么在touchstart中就会清除掉这个定时器,表示不是singleTap事件。

        touchTimeout && clearTimeout(touchTimeout)//这是在touchstart MSPointerDown pointerdown中的事件处理

然后如果250ms中继续触摸了屏幕,同时触摸事件间隔小于250ms就会成为doubleTap:

        if (delta > 0 && delta <= 250) touch.isDoubleTap = true

(2)从上面的代码可以知道,tap是立即执行的,但是singleTap,doubleTap是延迟执行的

tapTimeout = setTimeout(function() {// trigger universal 'tap' with the option to cancelTouch()// (cancelTouch cancels processing of single vs double taps for faster 'tap' response)var event = $.Event('tap')event.cancelTouch = cancelAlltouch.el.trigger(event)//tap会立即触发// trigger double tap immediately//如果是双击,那么我们立即触发doubleTap事件,这时候我们的touch={}那么后面的singleTap是不会执行的if (touch.isDoubleTap) {if (touch.el) touch.el.trigger('doubleTap')touch = {}}// trigger single tap after 250ms of inactivity//如果不是双击,那么此时tap已经触发了,等待250ms我们再次触发singleTap事件else {touchTimeout = setTimeout(function(){touchTimeout = nullif (touch.el) touch.el.trigger('singleTap')touch = {}//情况touch对象,因为手指已经离开屏幕了}, 250)}}, 0)} else {touch = {}}

(3)触屏端事件的执行顺序如下:

如果是单击的话:touchstart>touchend> tap>250ms>singleTap
如果是双击的话:touchstart>touchend> tap>touchstart>touchend> tap>doubleTap

第五步:我们看看最后的touchcancel MSPointerCancel pointercancel事件

  // when the browser window loses focus,// for example when a modal dialog is shown,// cancel all ongoing events//触摸被取消(触摸被一些事情中断,比如通知).on('touchcancel MSPointerCancel pointercancel', cancelAll)

其实该事件很简单,就是去掉所有的事件而已

function cancelAll() {//取消touch,tap,swipe,longTap事件//@touch 触屏事件//@tap 轻触事件//@swipe 滑动事件//@longTap 长点击事件if (touchTimeout) clearTimeout(touchTimeout)if (tapTimeout) clearTimeout(tapTimeout)if (swipeTimeout) clearTimeout(swipeTimeout)if (longTapTimeout) clearTimeout(longTapTimeout)touchTimeout = tapTimeout = swipeTimeout = longTapTimeout = nulltouch = {}}

第六步:我们总结一下各种事件触发的条件

doubleTap:移动的累积距离小于30;两次触摸屏幕时间区间为[0,250]

tap:移动的累积距离小于30,因为不能保证用于点击的时候没有移动;;立即触发

singleTap:移动的累积距离小于30;等待250ms后触发;

swipe:手指发生了移动,同时移动的距离在deltaX>30||deltaY>30,不过此处的deltaX和deltaY指的是手指落下的位置和最终手指的位置,而不是累积距离

longTap:手指长按超过了750ms。

下面是自己绘制的一张表,如有不正确的地方请拍砖:


也可以在空间查看

问题4:我们来看看FastClick.js的源码分析模块?

首先,我们看看那些元素需要触发原生的click事件(也就是不需要合成的click),也就是不需要我们来产生合成事件

 /*** Determine whether a given element requires a native click.如果是这下面的元素都需要触发浏览器的原生事件,button/select/textarea/input/label/iframe/video或者含有needsclick类名的元素都是需要触发原生的click事件,而不能因为300ms的延迟就不触发这个事件* @param {EventTarget|Element} target Target DOM element* @returns {boolean} Returns true if the element needs a native click*/FastClick.prototype.needsClick = function(target) {switch (target.nodeName.toLowerCase()) {// Don't send a synthetic click to disabled inputs (issue #62)case 'button':case 'select':case 'textarea':if (target.disabled) {//如果是disabled的元素,那么我们也是需要触发浏览器元素的click事件,而不用自己创建一个Event对象的return true;}break;case 'input':// File inputs need real clicks on iOS 6 due to a browser bug (issue #68)//文件输入框在IOS6上需要click事件if ((deviceIsIOS && target.type === 'file') || target.disabled) {return true;}break;case 'label':case 'iframe': // iOS8 homescreen apps can prevent events bubbling into frames//IOS8主屏幕程序能够阻止事件冒泡到frames,对于label,iframe,video都是不需要模拟一个事件的,只有用浏览器原生的click事件就ok了case 'video':return true;}//如果是needsclick的类名,这时候我们都是需要触发浏览器的原生click事件的return (/\bneedsclick\b/).test(target.className);};

上面的方法告诉我们

(1)如果是disabled的button/select/textarea元素,那么不需要触发合成的click事件,直接用原生的方法就可以了(因为disabled表示不能点击,那么减少300ms的延迟是没有意义的,直接使用浏览器的原生click就可以了);

(2)比如input元素,而且是disabled(因为disabled的input不能点击,那么减少300ms延迟没有意义);

(3)IOS下的file类型(single或者mutiple类型)也直接使用原生的click,其实是因为在ipad中照片选择界面存在的问题,在iphone中不存在问题,因为iphone中照片选择是全屏的;

(4)lable/iframe/video等也是应该使用原生的click的;

(5)使用了needsClick的类表示也是使用原生的click事件。

注意:如果是div等其他的元素通过这个函数返回的结果是false,表示还是会使用合成的click,那么他也是不存在300ms延迟问题的!

第二:我们看看那些元素在触发click之前需要调用focus方法

/*** Determine whether a given element requires a call to focus to simulate click into element.*判断一个元素是否需要调用focus()方法,然后才能去模拟在元素上的点击事件!如果返回true,那么在触发自己的click事件之前要手动调用focus()才行!* @param {EventTarget|Element} target Target DOM element* @returns {boolean} Returns true if the element requires a call to focus to simulate native click.*/FastClick.prototype.needsFocus = function(target) {switch (target.nodeName.toLowerCase()) {case 'textarea':return true;case 'select':return !deviceIsAndroid;//不是安卓的select需要case 'input':switch (target.type) {case 'button':case 'checkbox':case 'file':case 'image':case 'radio':case 'submit':return false;}// No point in attempting to focus disabled inputs//如果不是disabled同时也不是readOnly,这时候才需要返回调用focus方法来模拟click方法return !target.disabled && !target.readOnly;default://含有class="needsfocus"的元素必须手动调用focus来模仿元素的本地click方法return (/\bneedsfocus\b/).test(target.className);}};

(1)textarea元素;

(2)非android下的select元素;

(3)非disabled和非readOnly的input元素,同时不是button/checkbox/file/image/radio/submit;

(4)含有needFocus的元素。这些元素在触发合成的click事件之前需要手动调用focus方法才行。如下:

this.focus(targetElement);
this.sendClick(targetElement, event);

第三:如何让元素获取焦点的方法

/*** @param {EventTarget|Element} targetElement,也就是应该获取焦点的元素*/
FastClick.prototype.focus = function(targetElement) {var length;// Issue #160: on iOS 7, some input elements (e.g. date datetime month) throw a vague TypeError on setSelectionRange. //These elements don't have an integer value for the selectionStart and selectionEnd properties, but unfortunately that //can't be used for detection because accessing the properties also throws a TypeError. Just check the type instead. //Filed as Apple bug #15122724.//IOS7下,一些input元素,如data,time,month在调用setSelectionRange时候会抛出TypeError,这些元素的selectionStart/selectionEnd不是整数//而且没法验证,因为直接访问这些属性就会抛错了,因此我们直接检测typeif (deviceIsIOS && targetElement.setSelectionRange && targetElement.type.indexOf('date') !== 0 && targetElement.type !== 'time'&& targetElement.type !== 'month') {length = targetElement.value.length;//setSelectionRange用于设置input元素的开始和结束位置,https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/setSelectionRange//开始位置为length,结束位置也是length,表示光标移动到最后targetElement.setSelectionRange(length, length);} else {targetElement.focus();}
};

(1)在IOS7中,对于date,datetime,month类型的input元素,我们采用调用focus方法来完成获取焦点,而不是采用setSelectionRange,因为方法这个方法就会报错。

(2)对于其他元素使用setSelectionRange方法

第四:通过event.target来获取元素的目标对象

/*** @param {EventTarget} targetElement* @returns {Element|EventTarget}*/FastClick.prototype.getTargetElementFromEventTarget = function(eventTarget) {//在IOS4.1下,或者更老的浏览中,我们的target对象有可能是文本节点// On some older browsers (notably Safari on iOS 4.1 - see issue #56) the event target may be a text node.if (eventTarget.nodeType === Node.TEXT_NODE) {return eventTarget.parentNode;}return eventTarget;};

在老版本的IOS4.1中,event.target对象可能是 文本节点
第五:为targetElement元素添加 滚动的父元素作为属性,同时为滚动的父元素添加一个已经滚动的高度的属性

 /*** Check whether the given target element is a child of a scrollable layer and if so, set a flag on it.* @param {EventTarget|Element} targetElement* (1)为targetElement添加一个fastClickScrollParent属性,表示当前元素所在的滚动父元素* (2)为targetElement添加一个fastClickScrollParent属性的同时,为我们的fastClickScrollParent属性又添加一个fastClickLastScrollTop属性* 该属性表示滚动父元素当前已经滚动的scrollTop距离*/FastClick.prototype.updateScrollParent = function(targetElement) {var scrollParent, parentElement;//获取fastClickScrollParent属性scrollParent = targetElement.fastClickScrollParent;// Attempt to discover whether the target element is contained within a scrollable layer. Re-check if the// target element was moved to another parent.//用于判断一个指定的元素是否在一个scrollable layer中,如果目标元素移动到另外一个父元素时候又需要重新检查if (!scrollParent || !scrollParent.contains(targetElement)) {parentElement = targetElement;do {//如果scrollHeight>offsetHeight表示元素在垂直方向上存在滚动if (parentElement.scrollHeight > parentElement.offsetHeight) {scrollParent = parentElement;targetElement.fastClickScrollParent = parentElement;break;}//更新parentElement元素parentElement = parentElement.parentElement;} while (parentElement);}// Always update the scroll top tracker if possible.//获取到了滚动的父元素后,我们要更新scrollParent的fastClickLastScrollTop属性if (scrollParent) {scrollParent.fastClickLastScrollTop = scrollParent.scrollTop;}};

通过parentElement不断获取父元素,同时通过比较scrollHeight和offsetHeight来判断元素是否有滚动条;同时需要注意的是:如果targetElement本身是可以滚动,那么targetElement的fastClickScrollParent就是本身,同时targetElement的fastClickLastScrollTop就是表示自己已经滚动的距离了!

第六:如何判断触点是否变化

/*** Based on a touchmove event object, check whether the touch has moved past a boundary since it started.*判断touch是否移除了边界,在边界之外click就会被取消* @param {Event} event* @returns {boolean}*/FastClick.prototype.touchHasMoved = function(event) {var touch = event.changedTouches[0], boundary = this.touchBoundary;//changedTouches是涉及[当前事件]的触摸点的列表if (Math.abs(touch.pageX - this.touchStartX) > boundary || Math.abs(touch.pageY - this.touchStartY) > boundary) {return true;}return false;};

这里的touchStartX等属性在touchstart事件中被记录,同时如果触点大于我们配置的boundary,那么表示触点已经移动了。临界值是10px,如果大于10px那么表示触点已经移动了,当然这个值也可以自己设置!

第七:如何获取label元素指定的input元素

/*** Attempt to find the labelled control for the given label element.* @param {EventTarget|HTMLLabelElement} labelElement这里是labelElement元素作为参数* @returns {Element|null}*/FastClick.prototype.findControl = function(labelElement) {// Fast path for newer browsers supporting the HTML5 control attribute//html5为我们的labelElement元素指定了一个control属性,该属性表示该lable对应的input元素/*function setValue(){var label=document.getElementById("label");var textbox=label.control;//获取label元素的control属性,这时候获取到的就是我们的labelElement对应的input元素了textbox.value="718308";}*/if (labelElement.control !== undefined) {return labelElement.control;}// All browsers under test that support touch events also support the HTML5 htmlFor attributeif (labelElement.htmlFor) {return document.getElementById(labelElement.htmlFor);}// If no for attribute exists, attempt to retrieve the first labellable descendant element// the list of which is defined here: http://www.w3.org/TR/html5/forms.html#category-label//如果没有control,for属性,这时候我们就通过获取lable元素下面的button/keygen/meter等属性return labelElement.querySelector('button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea');};

注意:通过control属性,htmlFor属性,或者直接querySelector来一致获取for元素指定的input元素。同时这里展示了querySelector可以指定多个选择器,如果前面的选择器没有选中元素,那么就会自动使用后面的选择器去选择:

document.querySelector("#demos,#demoh,#demo").innerHTML = "Hello World1!";//如果#demos不存在就会选择#demoh

第八:那些情况下不需要FastClick来解决300ms的延迟问题?

(1)不支持touch事件,因为fastclick主要用于移动端的touch事件

(2)对于Android下的chrome浏览器,设置了user-scalable=no那么不需要fastClick;

(3)chrome32以及以上,如果有width<=device-width那么也不需要处理(也就是网页的宽度比浏览器的宽度小);

(4)chrome桌面浏览器不需要FastClick。

(5)黑莓10.3以上的系统,如果设置了meta[name=viewport],同时设置了user-scalable=no;黑莓10.3以上系统,width<=device-width都是没有延迟的;

(6)IE10以上的浏览器,同时设置了-ms-touch-action: none or manipulation,那么表示禁用了双击缩放效果,不具有延迟;

(7)IE11含有touch-action: none or manipulation也不具有300ms延迟问题

(8)FireFox浏览器大于27,同时含有meta[name=viewport]和user-scalable=no/width<device-width

注意:对于以上情况,我们不需要fastClick来解决300ms延迟问题;原因可能是本身就禁止了双击缩放,所以浏览器在第一次click后不需要等300ms判断是否是双击缩放,所以可以直接会自动触发浏览器原生的click!这种情况下,如果连续点击两次就相当于两次click!

/*** Check whether FastClick is needed.* @param {Element} layer The layer to listen on*/FastClick.notNeeded = function(layer) {var metaViewport;var chromeVersion;var blackberryVersion;var firefoxVersion;// Devices that don't support touch don't need FastClick//必须支持touch事件if (typeof window.ontouchstart === 'undefined') {return true;}// Chrome version - zero for other browsers//如果是chrome浏览器,那么我们可以获取到chrome的版本号,否则我们获取到的就是就是0!!!!chromeVersion = +(/Chrome\/([0-9]+)/.exec(navigator.userAgent) || [,0])[1];if (chromeVersion) {if (deviceIsAndroid) {metaViewport = document.querySelector('meta[name=viewport]');if (metaViewport) {// Chrome on Android with user-scalable="no" doesn't need FastClick (issue #89)//对于Android下的chrome浏览器,设置了user-scalable=no那么不需要fastClick!!!if (metaViewport.content.indexOf('user-scalable=no') !== -1) {return true;}// Chrome 32 and above with width=device-width or less don't need FastClick//chrome32上,如果有width<=device-width那么也不需要处理(width)if (chromeVersion > 31 && document.documentElement.scrollWidth <= window.outerWidth) {return true;}}// Chrome desktop doesn't need FastClick (issue #15)} else {//chrome桌面浏览器不需要FastClickreturn true;}}if (deviceIsBlackBerry10) {blackberryVersion = navigator.userAgent.match(/Version\/([0-9]*)\.([0-9]*)/);// BlackBerry 10.3+ does not require Fastclick library.// https://github.com/ftlabs/fastclick/issues/251//黑莓10.3以上的系统,如果设置了meta[name=viewport],同时设置了user-scalable=no/或者width<=device-width都是没有延迟的!if (blackberryVersion[1] >= 10 && blackberryVersion[2] >= 3) {metaViewport = document.querySelector('meta[name=viewport]');if (metaViewport) {// user-scalable=no eliminates click delay.if (metaViewport.content.indexOf('user-scalable=no') !== -1) {return true;}// width=device-width (or less than device-width) eliminates click delay.if (document.documentElement.scrollWidth <= window.outerWidth) {return true;}}}}// IE10 with -ms-touch-action: none or manipulation, which disables double-tap-to-zoom (issue #97)//IE10以上的浏览器,同时设置了-ms-touch-action: none or manipulation,那么表示禁用了双击缩放效果,不具有延迟if (layer.style.msTouchAction === 'none' || layer.style.touchAction === 'manipulation') {return true;}// Firefox version - zero for other browsersfirefoxVersion = +(/Firefox\/([0-9]+)/.exec(navigator.userAgent) || [,0])[1];//FireFox浏览器大于27,同时含有meta[name=viewport]和user-scalable=no/width<device-width。if (firefoxVersion >= 27) {// Firefox 27+ does not have tap delay if the content is not zoomable - https://bugzilla.mozilla.org/show_bug.cgi?id=922896metaViewport = document.querySelector('meta[name=viewport]');if (metaViewport && (metaViewport.content.indexOf('user-scalable=no') !== -1 || document.documentElement.scrollWidth <= window.outerWidth)) {return true;}}// IE11: prefixed -ms-touch-action is no longer supported and it's recomended to use non-prefixed version// http://msdn.microsoft.com/en-us/library/windows/apps/Hh767313.aspxif (layer.style.touchAction === 'none' || layer.style.touchAction === 'manipulation') {return true;}return false;};

问题5:我们看看fastClick中其他核心代码?

首先,我们要注意的就是ontouchstart方法

/*** On touch start, record the position and scroll offset.* 当触摸事件时候,我们记录下位置position和scroll滚动的距离* @param {Event} event* @returns {boolean}*/FastClick.prototype.onTouchStart = function(event) {var targetElement, touch, selection;// Ignore multiple touches, otherwise pinch-to-zoom is prevented if both fingers are on the FastClick element (issue #111).//不会同时跟踪两个触点的300ms问题(一次只能跟踪一个触点的点击延迟问题),否则手动放大缩小的问题就会被阻止了if (event.targetTouches.length > 1) {return true;}//获取触点元素,如果是TEXT_NODE那么获取其父元素targetElement = this.getTargetElementFromEventTarget(event.target);touch = event.targetTouches[0];//目标元素,也就是target元素上的触点if (deviceIsIOS) {// Only trusted events will deselect text on iOS (issue #49)//只有原生的Event在ISO中才会取消选择文本selection = window.getSelection();//如果选择了文本,我们也不会设置后面的trackingClick等if (selection.rangeCount && !selection.isCollapsed) {return true;}if (!deviceIsIOS4) {//当alert,confirm弹窗因为click事件弹出的时候,当下次用户点击页面中任何位置的时候,那么新的touchstart/touchend事件触发时候和上次click触发的事件//具有相同的identifier// Weird things happen on iOS when an alert or confirm dialog is opened from a click event callback (issue #23):// when the user next taps anywhere else on the page, new touchstart and touchend events are dispatched// with the same identifier as the touch event that previously triggered the click that triggered the alert.// Sadly, there is an issue on iOS 4 that causes some normal touch events to have the same identifier as an// immediately preceeding touch event (issue #52), so this fix is unavailable on that platform.//touch.identifier当Chrome的开发者工具打开的时候为0// Issue 120: touch.identifier is 0 when Chrome dev tools 'Emulate touch events' is set with an iOS device UA string,// which causes all touch events to be ignored. As this block only applies to iOS, and iOS identifiers are always long,// random integers, it's safe to to continue if the identifier is 0 here.if (touch.identifier && touch.identifier === this.lastTouchIdentifier) {event.preventDefault();return false;}this.lastTouchIdentifier = touch.identifier;// If the target element is a child of a scrollable layer (using -webkit-overflow-scrolling: touch) and://如果元素使用了-webkit-overflow-scrolling: touch事件:// 1) the user does a fling scroll on the scrollable layer// 2) the user stops the fling scroll with another tap// then the event.target of the last 'touchend' event will be the element that was under the user's finger// when the fling scroll was started, causing FastClick to send a click event to that layer - unless a check// is made to ensure that a parent layer was not scrolled before sending a synthetic click (issue #42).//当scroll滚动开始的时候,FastClick就会发送一个click事件,除非我们检查父元素在发送一个合成事件的时候并没有滚动!this.updateScrollParent(targetElement);}}//在touchstart中我们开始跟踪click事件this.trackingClick = true;//当前时间this.trackingClickStart = event.timeStamp;//targetElement元素this.targetElement = targetElement;//获取pageX,pageYthis.touchStartX = touch.pageX;this.touchStartY = touch.pageY;// Prevent phantom clicks on fast double-tap (issue #36)//如果两次点击之间小于200ms,那么我们阻止默认事件。不让click事件触发。这种情况出现只有可能是双击,否则不会只有几百毫秒//如果用户双击,那么我们也会取消掉默认的click事件,而采用自己模拟的click。this.lastClickTime只会在touchend中赋值if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {event.preventDefault();}return true;};

我们可以看到,我们是不会跟踪多个触点的,因为如果跟踪多个触点的click,那手动缩放可能会失效

// Ignore multiple touches, otherwise pinch-to-zoom is prevented if both fingers are on the FastClick element (issue #111).//不会同时跟踪两个触点的300ms问题(一次只能跟踪一个触点的点击延迟问题),否则手动放大缩小的问题就会被阻止了if (event.targetTouches.length > 1) {return true;}

如果两次点击的时间间隔小于200ms那么第二次click是不会触发的:

if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {event.preventDefault();
}

在IOS上,只有触发原生的click才能取消选择页面中的选中的内容,所以如果是这种情况我们直接返回而不用fastclick,而采用浏览器默认的click:

selection = window.getSelection();
if (selection.rangeCount && !selection.isCollapsed) {return true;
}

如果两次indentifier是一样,那么第二次的click直接忽略,也就是采用原生的click就可以了,如alert、confirm弹窗后的click 以及ios4中两次较快的点击导致相同的identifier:

if (touch.identifier && touch.identifier === this.lastTouchIdentifier) {event.preventDefault();return false;
}

第二:我们看看touchmove

 /*** Update the last position.* 更新最新的位置信息* @param {Event} event* @returns {boolean}*/FastClick.prototype.onTouchMove = function(event) {//trackingClick表示当前的click是否被跟踪,touchStart中设置为trueif (!this.trackingClick) {return true;}// If the touch has moved, cancel the click tracking//(1)如果touch已经移动那么我们取消click事件跟踪,或者touch已经在boundary之外那么我们也需要去除才行//这时候是移动,而不是点击,所以300ms延迟不需要跟踪//(2)targetElement是在touchStart中已经被设置了,在touchmove中我们重新计算当前的target对象,如果不相同,表示已经移动了,那么不需要跟踪click了//同时把targetElement置空if (this.targetElement !== this.getTargetElementFromEventTarget(event.target) || this.touchHasMoved(event)) {this.trackingClick = false;this.targetElement = null;}return true;};

如果触点已经移动了,那么我们就不会跟踪click事件,同时目标对象也会被设置为空。因为此时表示移动而不是点击
第三:我们看看touchend,其决定是否马上触发click事件

/*** On touch end, determine whether to send a click event at once.*touch end事件中判断是否应该马上触发click事件* @param {Event} event* @returns {boolean}*/FastClick.prototype.onTouchEnd = function(event) {var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement;//如果没有跟踪,也就是如用户点击后移动了坐标了/或者touchcancel了,那么原来的event.target的300ms就不需要处理了。if (!this.trackingClick) {return true;}// Prevent phantom clicks on fast double-tap (issue #36)//The minimum time between tap(touchstart and touchend) events//如果两次点击时间小于200ms,那么cancelNextClick设置为true//lastClickTime只会在onTouchEnd中进行设置,而event.timeStamp表示的是这一次触摸事件发生的时间//(1) this.lastClickTime只会在touchcancel中进行设置,因此,如果【两次touchend】触发的时候很短,那么表示双击了,因此我们就不需要跟踪后面的那一次click事件了//return表示也不会触发后面自定义的click事件了//(2)cancelNextClick只是用于touchEnd后用于mouseover/mousedown/mouseup等,用于判断是否调用stopPropagation/preventDefaultif ((event.timeStamp - this.lastClickTime) < this.tapDelay) {this.cancelNextClick = true;return true;}//click事件跟踪开始的时间,如果touchstart和touchEnd之间间隔的时间太久,那么也不会触发自定义click。例如长按if ((event.timeStamp - this.trackingClickStart) > this.tapTimeout) {return true;}// Reset to prevent wrong click cancel on input (issue #156).//重置cancelNextClick,防止对于input元素click事件的错误取消.this.cancelNextClick = false;//更新lastClickTime参数this.lastClickTime = event.timeStamp;//重置,trackingClick,trackingClickStarttrackingClickStart = this.trackingClickStart;this.trackingClick = false;//手指已经抬起,这时候不需要跟踪click了this.trackingClickStart = 0;// On some iOS devices, the targetElement supplied with the event is invalid if the layer// is performing a transition or scroll, and has to be re-detected manually. Note that// for this to function correctly, it must be called *after* the event target is checked!// See issue #57; also filed as rdar://13048589 .//IOS6-7:如果layer(也就是我们构造FastClick时候传入的DOM对象)执行transition/scroll时候,那么event对象提供的targetElement就是无效的,所以我们必须手动重新计算if (deviceIsIOSWithBadTarget) {touch = event.changedTouches[0];// In certain cases arguments of elementFromPoint can be negative, so prevent setting targetElement to null//elementFromPoint是获取当前元素相对于视口的位置targetElement = document.elementFromPoint(touch.pageX - window.pageXOffset, touch.pageY - window.pageYOffset) || targetElement;// *iOS 6.0-7.*需要我们手动设置目标元素,也就是target element!targetElement.fastClickScrollParent = this.targetElement.fastClickScrollParent;}targetTagName = targetElement.tagName.toLowerCase();if (targetTagName === 'label') {//获取for元素指定的元素forElement = this.findControl(targetElement);if (forElement) {this.focus(targetElement);//我们让for指定的元素获取焦点//(1)android:直接返回// (2)IOS:修改targetElement为for元素指定的元素if (deviceIsAndroid) {return false;}targetElement = forElement;}//如果触发自己定义的click事件之前,要手动调用focus方法才能模拟} else if (this.needsFocus(targetElement)) {// Case 1: If the touch started a while ago (best guess is 100ms based on tests for issue #36) then focus will be triggered anyway. //Return early and unset the target element reference so that the subsequent click will be allowed through.// Case 2: Without this exception for input elements tapped when the document is contained in an iframe, then any inputted text won't be visible //even though the value attribute is updated as the user types (issue #37).//Case 1:如果touch事件已经触发了,那么focus就会马上触发。马上返回,同时重置目标元素引用以便接下来的click事件能允许触发//Case 2:当我们的input元素处于iframe中同时被点击,那么所有的文本都是不可见的,即使value属性在用户输入的时候及时更新if ((event.timeStamp - trackingClickStart) > 100 || (deviceIsIOS && window.top !== window && targetTagName === 'input')) {this.targetElement = null;return false;}//首先给这个元素获取焦点,也就是先调用focus方法或者通过setSelection完成this.focus(targetElement);//focus后,我们在该元素上触发自定义的click事件this.sendClick(targetElement, event);// Select elements need the event to go through on iOS 4, otherwise the selector menu won't open.// Also this breaks opening selects when VoiceOver is active on iOS6, iOS7 (and possibly others)// var deviceIsIOS = /iP(ad|hone|od)/.test(navigator.userAgent) && !deviceIsWindowsPhone;//(1)如果不是IOS那么我们,那么我们可以阻止浏览器默认的‘click‘事件,同时把targetElement置为空// (2)如果是IOS,同时不是select,那么我们也可以阻止浏览器默认的'click'事件。也就是说IOS下的select必须让浏览器默认的click事件触发,否则select的选择面板不会弹出if (!deviceIsIOS || targetTagName !== 'select') {this.targetElement = null;event.preventDefault();}return false;}if (deviceIsIOS && !deviceIsIOS4) {// Don't send a synthetic click event if the target element is contained within a parent layer that was scrolled// and this tap is being used to stop the scrolling (usually initiated by a fling - issue #42).//(1)如果目标元素包含在一个parent layer中,而且该parent layer也被滚动了,那么我们就不会发送这个合成的click事件。这时候这个tap事件就用于阻止我们的scrolling事件scrollParent = targetElement.fastClickScrollParent;if (scrollParent && scrollParent.fastClickLastScrollTop !== scrollParent.scrollTop) {//表示在onTouchStart后又开始滚动了,表示父元素一直在滚动,这时候我们也不需要跟踪click的延迟,因为他会用于停止滚动return true;}}// Prevent the actual click from going though - unless the target node is marked as requiring// real clicks or if it is in the whitelist in which case only non-programmatic clicks are permitted.//如果needsClick返回true那么表示需要原生的click事件,于是在这里我们不会调用preventDefault方法!!!//如果不需要原生的方法,那么我们直接阻止原生的方法,阻止原生的方法的同时并调用sendClick来触发我们的模拟的方法if (!this.needsClick(targetElement)) {//(1)如果没有needsClick的class,那么表示会调用preventDefault取消浏览器默认的click事件,取而代之的是自己创建的click事件,而且这个事件是在targetEvent对象上触发的event.preventDefault();//我们在网上搜索fastClick,大部分都在说他解决了zepto的点击穿透问题,他是怎么解决的呢?就是上面最后一句,//他模拟的click事件是在touchEnd获取的真实元素上触发的,而不是通过坐标计算出来的元素(因为targetElement是一开始就保存好的,而不会是tap隐藏后而出现的弹窗下面的元素)。this.sendClick(targetElement, event);}return false;};

如果触点已经移动,或者touch事件已经取消,那么我们不需要触发自定义的事件

 //如果没有跟踪,也就是如用户点击后移动了坐标了/或者touchcancel了,那么原来的event.target的300ms就不需要处理了。if (!this.trackingClick) {return true;}

如果短时间点击了两次,那么我们不会跟踪第二次点击,从而直接忽略第二次

 //如果两次点击时间小于200ms,那么cancelNextClick设置为true//lastClickTime只会在onTouchEnd中进行设置,而event.timeStamp表示的是这一次触摸事件发生的时间//(1) this.lastClickTime只会在touchEnd中进行设置,因此,如果【两次touchend】触发的时候很短,那么表示双击了,因此我们就不需要跟踪后面的那一次click事件了//return表示也不会触发后面自定义的click事件了,这时候会触发浏览器默认的click事件//(2)cancelNextClick只是用于touchEnd后用于mouseover/mousedown/mouseup等,用于判断是否调用stopPropagation/preventDefaultif ((event.timeStamp - this.lastClickTime) < this.tapDelay) {this.cancelNextClick = true;return true;}

如果touchstart和touchend之间时间太久,那么就是长按了,也不会触发自定义的click

  //click事件跟踪开始的时间,如果touchstart和touchEnd之间间隔的时间太久,那么也不会触发自定义click。例如长按if ((event.timeStamp - this.trackingClickStart) > this.tapTimeout) {return true;}

如果layer在执行滚动或者动画,我们需要手动计算target

//IOS6-7:如果layer(也就是我们构造FastClick时候传入的DOM对象)执行transition/scroll时候,那么event对象提供的targetElement就是无效的//所以我们必须手动重新计算if (deviceIsIOSWithBadTarget) {touch = event.changedTouches[0];// In certain cases arguments of elementFromPoint can be negative, so prevent setting targetElement to null//elementFromPoint是获取当前元素相对于视口的位置targetElement = document.elementFromPoint(touch.pageX - window.pageXOffset, touch.pageY - window.pageYOffset) || targetElement;// *iOS 6.0-7.*需要我们手动设置目标元素,也就是target element!targetElement.fastClickScrollParent = this.targetElement.fastClickScrollParent;}

如果是label属性,那么我们让for元素成为targetElement。如果当前点击的是label标签,我们首先需要让label标签获取焦点,同时如果是安卓那么我们不需要触发click而直接返回(在思考着)如果是IOS更新targetElement为for指定的元素

                if (targetTagName === 'label') {//获取for元素指定的元素forElement = this.findControl(targetElement);if (forElement) {this.focus(targetElement);//我们让for指定的元素获取焦点//(1)android:直接返回// (2)IOS:修改targetElement为for元素指定的元素if (deviceIsAndroid) {return false;}targetElement = forElement;}//如果触发自己定义的click事件之前,要手动调用focus方法才能模拟}

如果在click之前需要获取焦点,那么我们先获取焦点然后触发合成的click事件。同时对于IOS下的select因为必须触发原生的click才会打开select标签,所以我们不会调用preventDefault方法

 if (this.needsFocus(targetElement)) {// Case 1: If the touch started a while ago (best guess is 100ms based on tests for issue #36) then focus will be triggered anyway. //Return early and unset the target element reference so that the subsequent click will be allowed through.// Case 2: Without this exception for input elements tapped when the document is contained in an iframe, then any inputted text won't be visible //even though the value attribute is updated as the user types (issue #37).//Case 1:如果touch事件已经触发了,那么focus就会马上触发。马上返回,同时重置目标元素引用以便接下来的click事件能允许触发//Case 2:当我们的input元素处于iframe中同时被点击,那么所有的文本都是不可见的,即使value属性在用户输入的时候及时更新if ((event.timeStamp - trackingClickStart) > 100 || (deviceIsIOS && window.top !== window && targetTagName === 'input')) {this.targetElement = null;return false;}//首先给这个元素获取焦点,也就是先调用focus方法或者通过setSelection完成this.focus(targetElement);//focus后,我们在该元素上触发自定义的click事件this.sendClick(targetElement, event);// Select elements need the event to go through on iOS 4, otherwise the selector menu won't open.// Also this breaks opening selects when VoiceOver is active on iOS6, iOS7 (and possibly others)//var deviceIsIOS = /iP(ad|hone|od)/.test(navigator.userAgent) && !deviceIsWindowsPhone;//在IOS下的select【必须】允许原生的click触发,否则select的菜单栏不会打开,同时把targetElement置为空(逆否命题)if (!deviceIsIOS || targetTagName !== 'select') {//这里是逆否命题的结果this.targetElement = null;event.preventDefault();}return false;}

如果非IOS4的iOS下,目标元素处于滚动的元素之中,那么第二次点击也会被忽略,因为这可能是停止滚动或者加速滚动而已。这可能也是为什么不用touchstart或者touchend来替代click的原因,你可以阅读后面的参考文献:

       if (deviceIsIOS && !deviceIsIOS4) {// Don't send a synthetic click event if the target element is contained within a parent layer that was scrolled// and this tap is being used to stop the scrolling (usually initiated by a fling - issue #42).scrollParent = targetElement.fastClickScrollParent;if (scrollParent && scrollParent.fastClickLastScrollTop !== scrollParent.scrollTop) {return true;}}

触发自定义的click事件

// Prevent the actual click from going though - unless the target node is marked as requiring// real clicks or if it is in the whitelist in which case only non-programmatic clicks are permitted.//如果needsClick返回true那么表示需要原生的click事件,于是在这里我们不会调用preventDefault方法!!!//如果不需要原生的方法,那么我们直接阻止原生的方法,阻止原生的方法的同时并调用sendClick来触发我们的模拟的方法if (!this.needsClick(targetElement)) {//(1)如果没有needsClick的class,那么表示会调用preventDefault取消浏览器默认的click事件,取而代之的是自己创建的click事件,而且这个事件是在targetEvent对象上触发的event.preventDefault();//我们在网上搜索fastClick,大部分都在说他解决了zepto的点击穿透问题,他是怎么解决的呢?就是上面最后一句,//他模拟的click事件是在touchEnd获取的真实元素上触发的,而不是通过坐标计算出来的元素(因为targetElement是一开始就保存好的,而不会是tap隐藏后而出现的弹窗下面的元素)。this.sendClick(targetElement, event);}

注意: 从这里你就会发现,fastClick是如何解决300ms的延迟问题的,其是通过取消默认事件后然后调用自己click事件来完成的。那么fastClick是如何 解决点击穿透问题的呢,其实就是下面的一句:

this.sendClick(targetElement, event);//target对象是保存好的,用来触发click事件的元素,而不是隐藏后位于底部的元素

下面是触发自定义事件的关键代码:

/*** Send a click event to the specified element.*为特定元素触发一个指定的click事件* @param {EventTarget|Element} targetElement* @param {Event} event*/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)//在一些安卓设备上,activeElement需要blur,否则同步的click事件无效。如果【当前具有焦点的元素和目标元素】不一致,那么要把焦点元素blur掉,否则//直接调用目标元素的sendClick是无效的if (document.activeElement && document.activeElement !== targetElement) {document.activeElement.blur();}// changedTouches:是涉及[当前事件]的触摸点的列表。touch = event.changedTouches[0];// Synthesise a click event, with an extra attribute so it can be trackedclickEvent = document.createEvent('MouseEvents');//直接调用dispatchEvent就可以了,但是需要获取到当前touch事件的screenX,screenY,clientX,clientY等属性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);};

总结:

下面我对那些情况下不会触发click进行了总结,你也可以仔细阅读上面的内容:

(1)   如果点击的时候移动了触点,那么不会触发click事件。其中移动的临界值是10px

 if (!this.trackingClick) {returntrue;}

(2) 如果是双击(两次点击小于200ms),那么第二次的click是不会触发的,其中delay是200ms

if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {event.preventDefault();}

(3)如果点击后持续了700ms不放开,那么不会触发fastclick的click,而是由浏览器自己处理,可能是弹出菜单栏:

if ((event.timeStamp -this.trackingClickStart) > this.tapTimeout) {returntrue;}

(4)IOS下,目标元素的父元素在滚动,那么第二次点击忽略

 if(deviceIsIOS && !deviceIsIOS4) {scrollParent= targetElement.fastClickScrollParent;if(scrollParent && scrollParent.fastClickLastScrollTop !==scrollParent.scrollTop) {returntrue;}}

(5)上面源码分析部分提到的8中情况(可以参考上面分析)

参考资源:

移动端click事件延迟300ms到底是怎么回事,该如何解决?

移动端300ms点击延迟和点击穿透问题

[Sencha ExtJS & Touch] singletap 和 tap的区别

tap事件是怎么模拟出来的?移动端触摸事件是怎么一个流程?

HTML5 手势检测原理和实现

突然发现一个问题,如果用touchstart替换了click 问题大了!?

在手持设备上使用 touchstart 事件代替 click 事件是不是个好主意?

也来说说touch事件与点击穿透问题

移动端开发基本知识之touch.js,FastClick.js源码分析相关推荐

  1. SpringBoot-web开发(四): SpringMVC的拓展、接管(源码分析)

    [SpringBoot-web系列]前文: SpringBoot-web开发(一): 静态资源的导入(源码分析) SpringBoot-web开发(二): 页面和图标定制(源码分析) SpringBo ...

  2. View的Touch事件分发(二.源码分析)

    Android中Touch事件的分发又分为View和ViewGroup的事件分发,先来看简单的View的touch事件分发. 主要分析View的dispatchTouchEvent()方法和onTou ...

  3. 深入理解 Node.js 中 EventEmitter源码分析(3.0.0版本)

    events模块对外提供了一个 EventEmitter 对象,即:events.EventEmitter. EventEmitter 是NodeJS的核心模块events中的类,用于对NodeJS中 ...

  4. commander.js使用及源码分析

    commander.js commander是一个轻巧的nodejs模块,提供了用户命令行输入和参数解析强大功能. commander的特性: 自记录代码 自动生成帮助 合并短参数 默认选项 强制选项 ...

  5. ViewGroup的Touch事件分发(源码分析)

    Android中Touch事件的分发又分为View和ViewGroup的事件分发,View的touch事件分发相对比较简单,可参考 View的Touch事件分发(一.初步了解) View的Touch事 ...

  6. netty 5 alph1源码分析(服务端创建过程)

    研究了netty的服务端创建过程.至于netty的优势,可以参照网络其他文章.<Netty系列之Netty 服务端创建>是 李林锋撰写的netty源码分析的一篇好文,绝对是技术干货.但抛开 ...

  7. java计算机毕业设计vue.js开发红酒网站MyBatis+系统+LW文档+源码+调试部署

    java计算机毕业设计vue.js开发红酒网站MyBatis+系统+LW文档+源码+调试部署 java计算机毕业设计vue.js开发红酒网站MyBatis+系统+LW文档+源码+调试部署 本源码技术栈 ...

  8. php企业官网源码 响应式,基于ThinkPHP5框架开发的响应式企业官网PHP源码_PC端+WAP手机端自适应+TP企业官网建站系统...

    源码介绍 基于ThinkPHP5框架开发的响应式企业官网PHP源码,是一款基于ThinkPHP5.0.10内核开发的企业建站管理系统,非常适合企业拿来二次开发自己的企业官网系统.前端界面采用流行的bo ...

  9. freertos源码详解与应用开发 pdf_互联网企业面试必问Spring源码?搞定Spring源码,看完这篇就够了...

    不用说,Spring已经成为Java后端开发的事实上的行业标准.无数公司选择Spring作为基本开发框架.大多数Java后端程序员在日常工作中也会接触到Spring.因此,如何很好地使用Spring, ...

最新文章

  1. 2021年最有用的数据清洗 Python 库
  2. 基于深度学习的文本分类应用!
  3. [LeetCode]--118. Pascal#39;s Triangle
  4. 如何面试.NET/ASP.NET工程师?
  5. 分行打印列表python_#python版一行内容分行输出
  6. java mongodb开发_Java Tutorial:Java操作MongoDB入门
  7. OpenGL ES的性能范围(OpenGL ES2.0官方文档)
  8. 【Python 安装】安装第三方库时 PermissionError: [WinError 5] Access is denied
  9. mysql重启服务命令_重启mysql命令
  10. 计算机考试反思1000,高一期中考试反思1000字,高一学生期中考试总结
  11. Android 项目实战视频资料 学习充电必备
  12. Android JetPack架构篇,一个实战项目带你学懂JetPack
  13. 【Mac】删除系统默认输入法
  14. Demo---progress-steps------ 2/50(详解)
  15. 黄金分割法python实现
  16. 计算机高中期末总结作文,期末考试总结作文(精选5篇)
  17. 错过了愚人节,还有清明节
  18. 计算机工业控制高职教材,计算机工业控制技术
  19. 关于Visual studio 2015 未能正确加载“Microsoft.VisualStudio.Editor.Implementation.EditorPackage”包的解决方案
  20. Python全栈之路---day01(背景、语法初识)

热门文章

  1. python编程入门pdf-Python游戏编程入门 中文pdf扫描版[41MB]
  2. HP3par 多路径存储磁盘使用方法
  3. RootExplorer怎么样获取root权限的
  4. TCP/IP 插口层
  5. 小白都能看懂的联想R720装WIN7系统记录
  6. Java GUI 阅读器之面板设计
  7. CentOS 7 在vmware中的网络设置详细介绍
  8. JpcapHandler——Jpcap抓包处理
  9. 随着BCPNP邀请分数不断居于高位,申请人境外直接申请难度加大,大部分申请人需要先入境工作再递交省提名申请
  10. Android 计算屏幕尺寸