大家都知道,因為歷史原因,移動端上的點選事件會有 300ms
左右的延遲,Zepto
的 touch
模組解決的就是移動端點選延遲的問題,同時也提供了滑動的 swipe
事件。
讀 Zepto 原始碼系列文章已經放到了github上,歡迎star: reading-zepto
原始碼版本
本文閱讀的原始碼為 zepto1.2.0
GitBook
實現的事件
;[`swipe`, `swipeLeft`, `swipeRight`, `swipeUp`, `swipeDown`,
`doubleTap`, `tap`, `singleTap`, `longTap`].forEach(function(eventName){
$.fn[eventName] = function(callback){ return this.on(eventName, callback) }
})
從上面的程式碼中可以看到,Zepto
實現了以下的事件:
- swipe: 滑動事件
- swipeLeft: 向左滑動事件
- swipeRight: 向右滑動事件
- swipeUp: 向上滑動事件
- swipeDown: 向下滑動事件
- doubleTap: 螢幕雙擊事件
- tap: 螢幕點選事件,比
click
事件響應更快 - singleTap: 螢幕單擊事件
- longTap: 長按事件
並且為每個事件都註冊了快捷方法。
內部方法
swipeDirection
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`)
}
返回的是滑動的方法。
x1
為 x軸
起點座標, x2
為 x軸
終點座標, y1
為 y軸
起點座標, y2
為 y軸
終點座標。
這裡有多組三元表示式,首先對比的是 x軸
和 y軸
上的滑動距離,如果 x軸
的滑動距離比 y軸
大,則為左右滑動,否則為上下滑動。
在 x軸
上,如果起點位置比終點位置大,則為向左滑動,返回 Left
,否則為向右滑動,返回 Right
。
在 y軸
上,如果起點位置比終點位置大,則為向上滑動,返回 Up
,否則為向下滑動,返回 Down
。
longTap
var touch = {},
touchTimeout, tapTimeout, swipeTimeout, longTapTimeout,
longTapDelay = 750,
gesture
function longTap() {
longTapTimeout = null
if (touch.last) {
touch.el.trigger(`longTap`)
touch = {}
}
}
觸發長按事件。
touch
物件儲存的是觸控過程中的資訊。
在觸發 longTap
事件前,先將儲存定時器的變數 longTapTimeout
釋放,如果 touch
物件中存在 last
,則觸發 longTap
事件, last
儲存的是最後觸控的時間。最後將 touch
重置為空物件,以便下一次使用。
cancelLongTap
function cancelLongTap() {
if (longTapTimeout) clearTimeout(longTapTimeout)
longTapTimeout = null
}
撤銷 longTap
事件的觸發。
如果有觸發 longTap
的定時器,清除定時器即可阻止 longTap
事件的觸發。
最後同樣需要將 longTapTimeout
變數置為 null
,等待垃圾回收。
cancelAll
function cancelAll() {
if (touchTimeout) clearTimeout(touchTimeout)
if (tapTimeout) clearTimeout(tapTimeout)
if (swipeTimeout) clearTimeout(swipeTimeout)
if (longTapTimeout) clearTimeout(longTapTimeout)
touchTimeout = tapTimeout = swipeTimeout = longTapTimeout = null
touch = {}
}
清除所有事件的執行。
其實就是清除所有相關的定時器,最後將 touch
物件設定為 null
。
isPrimaryTouch
function isPrimaryTouch(event){
return (event.pointerType == `touch` ||
event.pointerType == event.MSPOINTER_TYPE_TOUCH)
&& event.isPrimary
}
是否為主觸點。
當 pointerType
為 touch
並且 isPrimary
為 true
時,才為主觸點。 pointerType
可為 touch
、 pen
和 mouse
,這裡只處理手指觸控的情況。
isPointerEventType
function isPointerEventType(e, type){
return (e.type == `pointer`+type ||
e.type.toLowerCase() == `mspointer`+type)
}
觸發的是否為 pointerEvent
。
在低版本的移動端 IE 瀏覽器中,只實現了 PointerEvent
,並沒有實現 TouchEvent
,所以需要這個來判斷。
事件觸發
整體分析
$(document).ready(function(){
var now, delta, deltaX = 0, deltaY = 0, firstTouch, _isPointerType
$(document)
.bind(`MSGestureEnd`, function(e){
...
})
.on(`touchstart MSPointerDown pointerdown`, function(e){
...
})
.on(`touchmove MSPointerMove pointermove`, function(e){
...
})
.on(`touchend MSPointerUp pointerup`, function(e){
...
})
.on(`touchcancel MSPointerCancel pointercancel`, cancelAll)
$(window).on(`scroll`, cancelAll)
先來說明幾個變數,now
用來儲存當前時間, delta
用來儲存兩次觸控之間的時間差, deltaX
用來儲存 x軸
上的位移, deltaY
來用儲存 y軸
上的位移, firstTouch
儲存初始觸控點的資訊, _isPointerType
儲存是否為 pointerEvent
的判斷結果。
從上面可以看到, Zepto
所觸發的事件,是從 touch
、 pointer
或者 IE 的 guesture
事件中,根據不同情況計算出來的。這些事件都繫結在 document
上。
IE Gesture 事件的處理
IE
的手勢使用,需要經歷三步:
- 建立手勢物件
- 指定目標元素
- 指定手勢識別時需要處理的指標
if (`MSGesture` in window) {
gesture = new MSGesture()
gesture.target = document.body
}
這段程式碼包含了前兩步。
on(`touchstart MSPointerDown pointerdown`, function(e){
...
if (gesture && _isPointerType) gesture.addPointer(e.pointerId)
}
這段是第三步,用 addPointer
的方法,指定需要處理的指標。
bind(`MSGestureEnd`, function(e){
var swipeDirectionFromVelocity =
e.velocityX > 1 ? `Right` : e.velocityX < -1 ? `Left` : e.velocityY > 1 ? `Down` : e.velocityY < -1 ? `Up` : null
if (swipeDirectionFromVelocity) {
touch.el.trigger(`swipe`)
touch.el.trigger(`swipe`+ swipeDirectionFromVelocity)
}
})
接下來就是分析手勢了,Gesture
裡只處理 swipe
事件。
velocityX
和 velocityY
分別為 x軸
和 y軸
上的速率。這裡以 1
或 -1
為臨界點,判斷 swipe
的方向。
如果 swipe
的方向存在,則觸發 swipe
事件,同時也觸發帶方向的 swipe
事件。
start
on(`touchstart MSPointerDown pointerdown`, function(e){
if((_isPointerType = isPointerEventType(e, `down`)) &&
!isPrimaryTouch(e)) return
firstTouch = _isPointerType ? e : e.touches[0]
if (e.touches && e.touches.length === 1 && touch.x2) {
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)
touchTimeout && clearTimeout(touchTimeout)
touch.x1 = firstTouch.pageX
touch.y1 = firstTouch.pageY
if (delta > 0 && delta <= 250) touch.isDoubleTap = true
touch.last = now
longTapTimeout = setTimeout(longTap, longTapDelay)
if (gesture && _isPointerType) gesture.addPointer(e.pointerId)
})
過濾掉非觸屏事件
if((_isPointerType = isPointerEventType(e, `down`)) &&
!isPrimaryTouch(e)) return
firstTouch = _isPointerType ? e : e.touches[0]
這裡還將 isPointerEventType
的判斷結果儲存到了 _isPointerType
中,用來判斷是否為 PointerEvent
。
這裡的判斷其實就是隻處理 PointerEvent
和 TouchEvent
,並且 TouchEvent
的 isPrimary
必須為 true
。
因為 TouchEvent
支援多點觸碰,這裡只取觸碰的第一點存入 firstTouch
變數。
重置終點座標
if (e.touches && e.touches.length === 1 && touch.x2) {
touch.x2 = undefined
touch.y2 = undefined
}
如果還需要記錄,終點座標是需要更新的。
正常情況下,touch
物件會在 touchEnd
或者 cancel
的時候清空,但是如果使用者自己呼叫了 preventDefault
等,就可能會出現沒有清空的情況。
這裡有一點不太明白,為什麼只會在 touches
單點操作的時候才清空呢?多個觸碰點的時候不需要清空嗎?
記錄觸碰點的資訊
now = Date.now()
delta = now - (touch.last || now)
touch.el = $(`tagName` in firstTouch.target ?
firstTouch.target : firstTouch.target.parentNode)
touchTimeout && clearTimeout(touchTimeout)
touch.x1 = firstTouch.pageX
touch.y1 = firstTouch.pageY
now
用來儲存當前時間。
delta
用來儲存兩次點選時的時間間隔,用來處理雙擊事件。
touch.el
用來儲存目標元素,這裡有個判斷,如果 target
不是標籤節點時,取父節點作為目標元素。這會在點選偽類元素時出現。
如果 touchTimeout
存在,則清除定時器,避免重複觸發。
touch.x1
和 touch.y1
分別儲存 x軸
座標和 y軸
座標。
雙擊事件
if (delta > 0 && delta <= 250) touch.isDoubleTap = true
可以很清楚地看到, Zepto
將兩次點選的時間間隔小於 250ms
時,作為 doubleTap
事件處理,將 isDoubleTap
設定為 true
。
長按事件
touch.last = now
longTapTimeout = setTimeout(longTap, longTapDelay)
將 touch.last
設定為當前時間。這樣就可以記錄兩次點選時的時間差了。
同時開始長按事件定時器,從上面的程式碼可以看到,長按事件會在 750ms
後觸發。
move
on(`touchmove MSPointerMove pointermove`, function(e){
if((_isPointerType = isPointerEventType(e, `move`)) &&
!isPrimaryTouch(e)) return
firstTouch = _isPointerType ? e : e.touches[0]
cancelLongTap()
touch.x2 = firstTouch.pageX
touch.y2 = firstTouch.pageY
deltaX += Math.abs(touch.x1 - touch.x2)
deltaY += Math.abs(touch.y1 - touch.y2)
})
move
事件處理了兩件事,一是記錄終點座標,一是計算起點到終點之間的位移。
要注意這裡還呼叫了 cancelLongTap
清除了長按定時器,避免長按事件的觸發。因為有移動,肯定就不是長按了。
end
on(`touchend MSPointerUp pointerup`, function(e){
if((_isPointerType = isPointerEventType(e, `up`)) &&
!isPrimaryTouch(e)) return
cancelLongTap()
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)
else if (`last` in touch)
if (deltaX < 30 && deltaY < 30) {
tapTimeout = setTimeout(function() {
var event = $.Event(`tap`)
event.cancelTouch = cancelAll
if (touch.el) touch.el.trigger(event)
if (touch.isDoubleTap) {
if (touch.el) touch.el.trigger(`doubleTap`)
touch = {}
}
else {
touchTimeout = setTimeout(function(){
touchTimeout = null
if (touch.el) touch.el.trigger(`singleTap`)
touch = {}
}, 250)
}
}, 0)
} else {
touch = {}
}
deltaX = deltaY = 0
})
swipe
cancelLongTap()
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)
進入 end
時,立刻清除 longTap
定時器的執行。
可以看到,起點和終點的距離超過 30
時,會被判定為 swipe
滑動事件。
在觸發完 swipe
事件後,立即觸發對應方向上的 swipe
事件。
注意,swipe
事件並不是在 end
系列事件觸發時立即觸發的,而是設定了一個 0ms
的定時器,讓事件非同步觸發,這個有什麼用呢?後面會講到。
tap
else if (`last` in touch)
if (deltaX < 30 && deltaY < 30) {
tapTimeout = setTimeout(function() {
var event = $.Event(`tap`)
event.cancelTouch = cancelAll
if (touch.el) touch.el.trigger(event)
}, 0)
} else {
touch = {}
}
deltaX = deltaY = 0
終於看到重點了,首先判斷 last
是否存在,從 start
中可以看到,如果觸發了 start
, last
肯定是存在的,但是如果觸發了長按事件,touch
物件會被清空,這時不會再觸發 tap
事件。
如果不是 swipe
事件,也不存在 last
,則只將 touch
清空,不觸發任何事件。
在最後會將 deltaX
和 deltaY
重置為 0
。
觸發 tap
事件時,會在 event
中加了 cancelTouch
方法,外界可以通過這個方法取消所有事件的執行。
這裡同樣用了 setTimeout
非同步觸發事件。
doubleTap
if (touch.isDoubleTap) {
if (touch.el) touch.el.trigger(`doubleTap`)
touch = {}
}
這個 isDoubleTap
在 start
時確定的,上面已經分析過了,在 end
的時候觸發 doubleTap
事件。
因此,可以知道,在觸發 doubleTap
事件之前會觸發兩次 tap
事件。
singleTap
touchTimeout = setTimeout(function(){
touchTimeout = null
if (touch.el) touch.el.trigger(`singleTap`)
touch = {}
}, 250)
如果不是 doubleTap
,會在 tap
事件觸發的 250ms
後,觸發 singleTap
事件。
cancel
.on(`touchcancel MSPointerCancel pointercancel`, cancelAll)
在接受到 cancel
事件時,呼叫 cancelAll
方法,取消所有事件的觸發。
scroll
$(window).on(`scroll`, cancelAll)
從前面的分析可以看到,所有的事件觸發都是非同步的。
因為在 scroll
的時候,肯定是隻想響應滾動的事件,非同步觸發是為了在 scroll
的過程中和外界呼叫 cancelTouch
方法時, 可以將事件取消。
系列文章
- 讀Zepto原始碼之程式碼結構
- 讀Zepto原始碼之內部方法
- 讀Zepto原始碼之工具函式
- 讀Zepto原始碼之神奇的$
- 讀Zepto原始碼之集合操作
- 讀Zepto原始碼之集合元素查詢
- 讀Zepto原始碼之操作DOM
- 讀Zepto原始碼之樣式操作
- 讀Zepto原始碼之屬性操作
- 讀Zepto原始碼之Event模組
- 讀Zepto原始碼之IE模組
- 讀Zepto原始碼之Callbacks模組
- 讀Zepto原始碼之Deferred模組
- 讀Zepto原始碼之Ajax模組
- 讀Zepto原始碼之Assets模組
- 讀Zepto原始碼之Selector模組
參考
- zepto touch 庫原始碼分析
- PointerEvent
- Pointer events
- TouchEvent
- Touch
- GestureEvent
- MSGestureEvent
- 一步一步DIY zepto庫,研究zepto原始碼8–touch模組
- zepto原始碼學習-06 touch
- zepto原始碼之touch.js
- addPointer method.aspx)
License
署名-非商業性使用-禁止演繹 4.0 國際 (CC BY-NC-ND 4.0)
最後,所有文章都會同步傳送到微信公眾號上,歡迎關注,歡迎提意見:
作者:對角另一面