移動端原生支持
touchstart
、touchmove
、touchend
等事件,可是在日常業務中咱們常常須要使用swipe
、tap
、doubleTap
、longTap
等事件去實現想要的效果,對於這種自定義事件他們底層是如何實現的呢?讓咱們從Zepto.js
的touch
模塊去分析其原理。您也能夠直接查看touch.js源碼註釋javascript
源碼倉庫java
原文連接git
Zepto的touch模塊實現了不少與手勢相關的自定義事件,分別是
swipe
,swipeLeft
,swipeRight
,swipeUp
,swipeDown
,doubleTap
,tap
,singleTap
,longTap
github
事件名稱 | 事件描述 |
---|---|
swipe | 滑動事件 |
swipeLeft | ←左滑事件 |
swipeRight | →右滑事件 |
swipeUp | ↑上滑事件 |
swipeDown | ↓下滑事件 |
doubleTap | 雙擊事件 |
tap | 點擊事件(非原生click事件) |
singleTap | 單擊事件 |
longTap | 長按事件 |
;['swipe', 'swipeLeft', 'swipeRight', 'swipeUp', 'swipeDown', 'doubleTap', 'tap', 'singleTap', 'longTap'].forEach(function(eventName){
$.fn[eventName] = function(callback){ return this.on(eventName, callback) }
})
複製代碼
能夠看到Zepto把這些方法都掛載到了原型上,這意味着,你能夠直接用簡寫的方式例如$('body').tap(callback)
面試
在開始分析這些事件如何實現以前,咱們先了解一些前置條件ajax
var touch = {},
touchTimeout, tapTimeout, swipeTimeout, longTapTimeout,
// 長按事件定時器時間
longTapDelay = 750,
gesture
複製代碼
touch
: 用以存儲手指操做的相關信息,例如手指按下時的位置,離開時的座標等。json
touchTimeout
,tapTimeout
, swipeTimeout
,longTapTimeout
分別存儲singleTap、tap、swipe、longTap事件的定時器。瀏覽器
longTapDelay
:longTap事件定時器延時時間緩存
gesture
: 存儲ieGesture事件對象框架
咱們根據下圖以及對應的代碼來理解滑動的時候方向是如何斷定的。須要注意的是瀏覽器中的「座標系」和數學中的座標系仍是不太同樣,Y軸有點反過來的意思。
/** * 判斷移動的方向,結果是Left, Right, Up, Down中的一個 * @param {} x1 起點的橫座標 * @param {} x2 終點的橫座標 * @param {} y1 起點的縱座標 * @param {} y2 終點的縱座標 */
function swipeDirection(x1, x2, y1, y2) {
/** * 1. 第一個三元運算符獲得若是x軸滑動的距離比y軸大,那麼是左右滑動,不然是上下滑動 * 2. 若是是左右滑動,起點比終點大那麼往左滑動 * 3. 若是是上下滑動,起點比終點大那麼往上滑動 * 須要注意的是這裏的座標和數學中的有些不必定 縱座標有點反過來的意思 * 起點p1(1, 0) 終點p2(1, 1) */
return Math.abs(x1 - x2) >=
Math.abs(y1 - y2) ? (x1 - x2 > 0 ? 'Left' : 'Right') : (y1 - y2 > 0 ? 'Up' : 'Down')
}
複製代碼
function longTap() {
longTapTimeout = null
if (touch.last) {
// 觸發el元素的longTap事件
touch.el.trigger('longTap')
touch = {}
}
}
複製代碼
在觸發長按事件以前先將longTapTimeout
定時器取消,若是touch.last
還存在則觸發之,爲何要判斷touch.last
呢,由於swip
, doubleTap
,singleTap
會將touch對象置空,當這些事件發生的時候,天然不該該發生長按事件。
// 取消長按
function cancelLongTap() {
if (longTapTimeout) clearTimeout(longTapTimeout)
longTapTimeout = null
}
// 取消全部事件
function cancelAll() {
if (touchTimeout) clearTimeout(touchTimeout)
if (tapTimeout) clearTimeout(tapTimeout)
if (swipeTimeout) clearTimeout(swipeTimeout)
if (longTapTimeout) clearTimeout(longTapTimeout)
touchTimeout = tapTimeout = swipeTimeout = longTapTimeout = null
touch = {}
}
複製代碼
方式都是相似,先調用clearTimeout取消定時器,而後釋放對應的變量,等候垃圾回收。
$(document).ready(function(){
/** * now 當前觸摸時間 * delta 兩次觸摸的時間差 * deltaX x軸變化量 * deltaY Y軸變化量 * firstTouch 觸摸點相關信息 * _isPointerType 是不是pointerType */
var now, delta, deltaX = 0, deltaY = 0, firstTouch, _isPointerType
$(document)
.bind('MSGestureEnd', function(e){
// xxx 先不看這裏
})
.on('touchstart MSPointerDown pointerdown', function(e){
// xxx 關注這裏
})
.on('touchmove MSPointerMove pointermove', function(e){
// xxx 關注這裏
})
.on('touchend MSPointerUp pointerup', function(e){
// xxx 關注這裏
})
.on('touchcancel MSPointerCancel pointercancel', cancelAll)
$(window).on('scroll', cancelAll)
})
複製代碼
這裏將詳細代碼暫時省略了,留出總體框架,能夠看出Zepto在dom,ready的時候在document
上添加了MSGestureEnd
,touchstart MSPointerDown pointerdown
,touchmove MSPointerMove pointermove
,touchcancel MSPointerCancel pointercancel
等事件,最後還給在window上加了scroll
事件。咱們將目光聚焦在touchstart
,touchmove
,touchend
對應的邏輯,其餘相對少見的事件在暫不討論
if((_isPointerType = isPointerEventType(e, 'down'))
&& !isPrimaryTouch(e)) return
複製代碼
要走到touchstart
事件處理程序後續邏輯中,須要先知足一些條件。究竟是哪些條件呢?先來看看isPointerEventType
, isPrimaryTouch
兩個函數作了些什麼。
**isPointerEventType
function isPointerEventType(e, type){
return (e.type == 'pointer'+type ||
e.type.toLowerCase() == 'mspointer'+type)
}
複製代碼
Pointer Event相關知識點擊這裏
isPrimaryTouch
function isPrimaryTouch(event){
return (event.pointerType == 'touch' ||
event.pointerType == event.MSPOINTER_TYPE_TOUCH)
&& event.isPrimary
}
複製代碼
根據mdn pointerType,其類型能夠是mouse
,pen
,touch
,這裏只處理其值爲touch而且isPrimary爲true的狀況。
接着回到
if((_isPointerType = isPointerEventType(e, 'down'))
&& !isPrimaryTouch(e)) return
複製代碼
其實就是過濾掉非觸摸事件。
觸摸點信息兼容處理
// 若是是pointerdown事件則firstTouch保存爲e,不然是e.touches第一個
firstTouch = _isPointerType ? e : e.touches[0]
複製代碼
這裏只清楚e.touches[0]
的處理邏輯,另外一種不太明白,望有知曉的同窗告知一下,感謝感謝。
復原終點座標
// 通常狀況下,在touchend或者cancel的時候,會將其清除,若是用戶調阻止了默認事件,則有可能清空不了,可是爲何要將終點座標清除呢?
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.
touch.x2 = undefined
touch.y2 = undefined
}
複製代碼
存儲觸摸點部分信息
// 保存當前時間
now = Date.now()
// 保存兩次點擊時候的時間間隔,主要用做雙擊事件
delta = now - (touch.last || now)
// touch.el 保存目標節點
// 不是標籤節點則使用該節點的父節點,注意有僞元素
touch.el = $('tagName' in firstTouch.target ?
firstTouch.target : firstTouch.target.parentNode)
// touchTimeout 存在則清除之,能夠避免重複觸發
touchTimeout && clearTimeout(touchTimeout)
// 記錄起始點座標(x1, y1)(x軸,y軸)
touch.x1 = firstTouch.pageX
touch.y1 = firstTouch.pageY
複製代碼
判斷雙擊事件
// 兩次點擊的時間間隔 > 0 且 < 250 毫秒,則當作doubleTap事件處理
if (delta > 0 && delta <= 250) touch.isDoubleTap = true
複製代碼
處理長按事件
// 將now設置爲touch.last,方便上面能夠計算兩次點擊的時間差
touch.last = now
// longTapDelay(750毫秒)後觸發長按事件
longTapTimeout = setTimeout(longTap, longTapDelay)
複製代碼
.on('touchmove MSPointerMove pointermove', function(e){
if((_isPointerType = isPointerEventType(e, 'move')) &&
!isPrimaryTouch(e)) return
firstTouch = _isPointerType ? e : e.touches[0]
// 取消長按事件,都移動了,固然不是長按了
cancelLongTap()
// 終點座標 (x2, y2)
touch.x2 = firstTouch.pageX
touch.y2 = firstTouch.pageY
// 分別記錄X軸和Y軸的變化量
deltaX += Math.abs(touch.x1 - touch.x2)
deltaY += Math.abs(touch.y1 - touch.y2)
})
複製代碼
手指移動的時候,作了三件事情。
.on('touchend MSPointerUp pointerup', function(e){
if((_isPointerType = isPointerEventType(e, 'up')) &&
!isPrimaryTouch(e)) return
// 取消長按事件
cancelLongTap()
// 滑動事件,只要X軸或者Y軸的起始點和終點的距離超過30則認爲是滑動,並觸發滑動(swip)事件,
// 緊接着立刻觸發對應方向的swip事件(swipLeft, swipRight, swipUp, swipDown)
// swipe
if ((touch.x2 && Math.abs(touch.x1 - touch.x2) > 30) ||
(touch.y2 && Math.abs(touch.y1 - touch.y2) > 30))
swipeTimeout = setTimeout(function() {
if (touch.el){
touch.el.trigger('swipe')
touch.el.trigger('swipe' + (swipeDirection(touch.x1, touch.x2, touch.y1, touch.y2)))
}
touch = {}
}, 0)
// touch對象的last屬性,在touchstart事件中添加,因此觸發了start事件便會存在
// normal tap
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
// 只有當X軸和Y軸的變化量都小於30的時候,才認爲有可能觸發tap事件
if (deltaX < 30 && deltaY < 30) {
// delay by one tick so we can cancel the 'tap' event if 'scroll' fires
// ('tap' fires before 'scroll')
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')
// 往自定義事件中添加cancelTouch回調函數,這樣使用者能夠經過該方法取消全部的事件
event.cancelTouch = cancelAll
// [by paper] fix -> "TypeError: 'undefined' is not an object (evaluating 'touch.el.trigger'), when double tap
// 當目標元素存在,觸發tap自定義事件
if (touch.el) touch.el.trigger(event)
// trigger double tap immediately
// 若是是doubleTap事件,則觸發之,並清除touch
if (touch.isDoubleTap) {
if (touch.el) touch.el.trigger('doubleTap')
touch = {}
}
// trigger single tap after 250ms of inactivity
// 不然在250毫秒以後。觸發單擊事件
else {
touchTimeout = setTimeout(function(){
touchTimeout = null
if (touch.el) touch.el.trigger('singleTap')
touch = {}
}, 250)
}
}, 0)
} else {
// 不是tap相關的事件
touch = {}
}
// 最後將變化量信息清空
deltaX = deltaY = 0
})
複製代碼
touchend事件觸發時,相應的註釋都在上面了,可是咱們來分解一下這段代碼。
swip事件相關
if ((touch.x2 && Math.abs(touch.x1 - touch.x2) > 30) ||
(touch.y2 && Math.abs(touch.y1 - touch.y2) > 30))
swipeTimeout = setTimeout(function() {
if (touch.el){
touch.el.trigger('swipe')
touch.el.trigger('swipe' + (swipeDirection(touch.x1, touch.x2, touch.y1, touch.y2)))
}
touch = {}
}, 0)
複製代碼
手指離開後,經過判斷x軸或者y軸的位移,只要其中一個跨度大於30便會觸發swip
及其對應方向的事件。
tap,doubleTap,singleTap
這三個事件可能觸發的前提條件是touch對象中還存在last屬性,從touchstart事件處理程序中知道last在其中記錄,而在touchend以前被清除的時機是長按事件被觸發longTap
,取消全部事件被調用cancelAll
if (deltaX < 30 && deltaY < 30) {
// delay by one tick so we can cancel the 'tap' event if 'scroll' fires
// ('tap' fires before 'scroll')
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
// [by paper] fix -> "TypeError: 'undefined' is not an object (evaluating 'touch.el.trigger'), when double tap
if (touch.el) touch.el.trigger(event)
}
}
複製代碼
只有當x軸和y軸的變化量都小於30的時候纔會觸發tap
事件,注意在觸發tap事件以前,Zepto還將往事件對象上添加了cancelTouch屬性,對應的也就是cancelAll方法,即你能夠經過他取消全部的touch相關事件。
// trigger double tap immediately
if (touch.isDoubleTap) {
if (touch.el) touch.el.trigger('doubleTap')
touch = {}
}
// trigger single tap after 250ms of inactivity
else {
touchTimeout = setTimeout(function(){
touchTimeout = null
if (touch.el) touch.el.trigger('singleTap')
touch = {}
}, 250)
}
複製代碼
在發生觸發tap事件以後,若是是doubleTap,則會緊接着觸發doubleTap事件,不然250毫秒以後觸發singleTap事件,而且都會講touch對象置爲空對象,以便下次使用
// 最後將變化量信息清空
deltaX = deltaY = 0
複製代碼
.on('touchcancel MSPointerCancel pointercancel', cancelAll)
複製代碼
當touchcancel
被觸發的時候,取消全部的事件。
$(window).on('scroll', cancelAll)
複製代碼
當滾動事件被觸發的時候,取消全部的事件(這裏有些不解,滾動事件觸發,徹底有多是要觸發tap或者swip等事件啊)。
最後說一個面試中常常會問的問題,touch擊穿現象。若是對此有興趣能夠查看移動端click延遲及zepto的穿透現象, 新年第一發--深刻不淺出zepto的Tap擊穿問題
touch.js
ie.js
data.js
form.js
zepto.js
event.js
ajax.js