移动端开发基本知识之touch.js,FastClick.js源码分析
問題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等事件類型
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源码分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 快递柜APP开发需要注意的地方
- 下一篇: RDM连接Redis配置