问题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中列出了三种方法。
第一种方法:
这个方案有一个缺点,就是必须通过完全禁用缩放来达到去掉点击延迟的目的,然而完全禁用缩放并不是我们的初衷,我们只是想禁掉默认的双击缩放行为,这样就不用等待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'+type
function 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.aspx
if ('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.html
firstTouch = _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 = undefined
touch.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.pageX
touch.y1 = firstTouch.pageY
//x1,y1存储的是pageX,pageY属性
if (delta > 0 && delta <= 250) touch.isDoubleTap = true
//如果两次触屏在[0,250]表示双击
touch.last = now
longTapTimeout = setTimeout(longTap, longTapDelay)
//这里是长按
// adds the current touch contact for IE gesture recognition
if (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)) return
firstTouch = _isPointerType ? e : e.touches[0]
//这时候我们取消长按的事件处理程序,因为这里是pointermove,也就是手势移动了那么肯定不会是长按了
//但是因为我们在pointerdown中不知道是否是长按还是pointermove,所以才默认使用的长按。如果移动了,那么就知道不是长按了
cancelLongTap()
touch.x2 = firstTouch.pageX
touch.y2 = firstTouch.pageY
deltaX += 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()
// swipe
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)
// 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 = cancelAll
touch.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 = null
if (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.aspx
if ('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
.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.html
firstTouch = _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 = undefined
touch.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.pageX
touch.y1 = firstTouch.pageY
//x1,y1存储的是pageX,pageY属性
if (delta > 0 && delta <= 250) touch.isDoubleTap = true
//如果两次触屏在[0,250]表示双击
touch.last = now
longTapTimeout = setTimeout(longTap, longTapDelay)
//这里是长按
// adds the current touch contact for IE gesture recognition
if (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.pageX
touch.y1 = firstTouch.pageY
第三步:我们看看
touchmove MSPointerMove pointermove等事件
.on('touchmove MSPointerMove pointermove', function(e){
if((_isPointerType = isPointerEventType(e, 'move')) &&
!isPrimaryTouch(e)) return
firstTouch = _isPointerType ? e : e.touches[0]
//这时候我们取消长按的事件处理程序,因为这里是pointermove,也就是手势移动了那么肯定不会是长按了
//但是因为我们在pointerdown中不知道是否是长按还是pointermove,所以才默认使用的长按。如果移动了,那么就知道不是长按了
cancelLongTap()
touch.x2 = firstTouch.pageX
touch.y2 = firstTouch.pageY
deltaX += Math.abs(touch.x1 - touch.x2)
deltaY += Math.abs(touch.y1 - touch.y2)
//得到两者在X和Y方向移动的绝对距离
})
首先:因为触点已经移动,所以我们要取消一开始为长按的假设
cancelLongTap()
然后:判断触点移动的终点位置,然后记录触点变换的距离大小
touch.x2 = firstTouch.pageX
touch.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()
// swipe
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)
// 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 = cancelAll
touch.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 = null
if (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 = cancelAll
touch.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 = null
if (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 = null
if (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 = cancelAll
touch.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 = null
if (touch.el) touch.el.trigger('singleTap')
touch = {}
//情况touch对象,因为手指已经离开屏幕了
}, 250)
}
}, 0)
} else {
touch = {}
}
(3)触屏端事件的执行顺序如下:
如果是单击的话:touchstart>touchend> tap>250ms>singleTap第五步:我们看看最后的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 = null
touch = {}
}
第六步:我们总结一下各种事件触发的条件
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不是整数
//而且没法验证,因为直接访问这些属性就会抛错了,因此我们直接检测type
if (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对象可能是
文本节点。
/**
* 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 attribute
if (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 注意:对于以上情况,我们不需要fastClick来解决300ms延迟问题;原因可能是本身就禁止了双击缩放,所以浏览器在第一次click后不需要等300ms判断是否是双击缩放,所以可以直接会自动触发浏览器原生的click!这种情况下,如果连续点击两次就相当于两次click! 问题5:我们看看fastClick中其他核心代码? 首先,我们要注意的就是ontouchstart方法 如果两次点击的时间间隔小于200ms那么第二次click是不会触发的: 在IOS上,只有触发原生的click才能取消选择页面中的选中的内容,所以如果是这种情况我们直接返回而不用fastclick,而采用浏览器默认的click: 如果两次indentifier是一样,那么第二次的click直接忽略,也就是采用原生的click就可以了,如alert、confirm弹窗后的click 以及ios4中两次较快的点击导致相同的identifier: 第二:我们看看touchmove 如果是label属性,那么我们让for元素成为targetElement。如果当前点击的是label标签,我们首先需要让label标签获取焦点,同时如果是安卓那么我们不需要触发click而直接返回(在思考着)!如果是IOS更新targetElement为for指定的元素: 如果非IOS4的iOS下,目标元素处于滚动的元素之中,那么第二次点击也会被忽略,因为这可能是停止滚动或者加速滚动而已。这可能也是为什么不用touchstart或者touchend来替代click的原因,你可以阅读后面的参考文献: 触发自定义的click事件 下面是触发自定义事件的关键代码: 总结: 下面我对那些情况下不会触发click进行了总结,你也可以仔细阅读上面的内容: (1) 如果点击的时候移动了触点,那么不会触发click事件。其中移动的临界值是10px (2) 如果是双击(两次点击小于200ms),那么第二次的click是不会触发的,其中delay是200ms (3)如果点击后持续了700ms不放开,那么不会触发fastclick的click,而是由浏览器自己处理,可能是弹出菜单栏: (4)IOS下,目标元素的父元素在滚动,那么第二次点击忽略 参考资源: 移动端click事件延迟300ms到底是怎么回事,该如何解决? 移动端300ms点击延迟和点击穿透问题 [Sencha ExtJS & Touch] singletap 和 tap的区别 tap事件是怎么模拟出来的?移动端触摸事件是怎么一个流程? HTML5 手势检测原理和实现 突然发现一个问题,如果用touchstart替换了click 问题大了!? 在手持设备上使用 touchstart 事件代替 click 事件是不是个好主意? 也来说说touch事件与点击穿透问题/**
* 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桌面浏览器不需要FastClick
return 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 browsers
firefoxVersion = +(/Firefox\/([0-9]+)/.exec(navigator.userAgent) || [,0])[1];
//FireFox浏览器大于27,同时含有meta[name=viewport]和user-scalable=no/width
我们可以看到,我们是不会跟踪多个触点的,因为如果跟踪多个触点的click,那手动缩放可能会失效
/**
* 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,pageY
this.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;
};
// 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;
}
if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {
event.preventDefault();
}
selection = window.getSelection();
if (selection.rangeCount && !selection.isCollapsed) {
return true;
}
if (touch.identifier && touch.identifier === this.lastTouchIdentifier) {
event.preventDefault();
return false;
}
如果触点已经移动了,那么我们就不会跟踪click事件,同时目标对象也会被设置为空。因为此时表示移动而不是点击
/**
* Update the last position.
* 更新最新的位置信息
* @param {Event} event
* @returns {boolean}
*/
FastClick.prototype.onTouchMove = function(event) {
//trackingClick表示当前的click是否被跟踪,touchStart中设置为true
if (!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;
};
第三:我们看看touchend,其决定是否马上触发click事件
如果触点已经移动,或者touch事件已经取消,那么我们不需要触发自定义的事件
/**
* 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/preventDefault
if ((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,trackingClickStart
trackingClickStart = 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;
};
如果短时间点击了两次,那么我们不会跟踪第二次点击,从而直接忽略第二次
//如果没有跟踪,也就是如用户点击后移动了坐标了/或者touchcancel了,那么原来的event.target的300ms就不需要处理了。
if (!this.trackingClick) {
return true;
}
如果touchstart和touchend之间时间太久,那么就是长按了,也不会触发自定义的click
//如果两次点击时间小于200ms,那么cancelNextClick设置为true
//lastClickTime只会在onTouchEnd中进行设置,而event.timeStamp表示的是这一次触摸事件发生的时间
//(1) this.lastClickTime只会在touchEnd中进行设置,因此,如果【两次touchend】触发的时候很短,那么表示双击了,因此我们就不需要跟踪后面的那一次click事件了
//return表示也不会触发后面自定义的click事件了,这时候会触发浏览器默认的click事件
//(2)cancelNextClick只是用于touchEnd后用于mouseover/mousedown/mouseup等,用于判断是否调用stopPropagation/preventDefault
if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {
this.cancelNextClick = true;
return true;
}
如果layer在执行滚动或者动画,我们需要手动计算target
//click事件跟踪开始的时间,如果touchstart和touchEnd之间间隔的时间太久,那么也不会触发自定义click。例如长按
if ((event.timeStamp - this.trackingClickStart) > this.tapTimeout) {
return true;
}
//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;
}
如果在click之前需要获取焦点,那么我们先获取焦点然后触发合成的click事件。同时对于IOS下的select因为必须触发原生的click才会打开select标签,所以我们不会调用preventDefault方法
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方法才能模拟
}
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;
}
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;
}
}
注意:
从这里你就会发现,fastClick是如何解决300ms的延迟问题的,其是通过取消默认事件后然后调用自己click事件来完成的。那么fastClick是如何
解决点击穿透问题的呢,其实就是下面的一句:
// 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);
}
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 tracked
clickEvent = 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);
};
if (!this.trackingClick) {
returntrue;
}
if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {
event.preventDefault();
}
if ((event.timeStamp -this.trackingClickStart) > this.tapTimeout) {
returntrue;
}
(5)上面源码分析部分提到的8中情况(可以参考上面分析)
if(deviceIsIOS && !deviceIsIOS4) {
scrollParent= targetElement.fastClickScrollParent;
if(scrollParent && scrollParent.fastClickLastScrollTop !==scrollParent.scrollTop) {
returntrue;
}
}