讀Zepto源碼之Touch模塊

你們都知道,由於歷史緣由,移動端上的點擊事件會有 300ms 左右的延遲,Zeptotouch 模塊解決的就是移動端點擊延遲的問題,同時也提供了滑動的 swipe 事件。javascript

讀 Zepto 源碼系列文章已經放到了github上,歡迎star: reading-zeptojava

源碼版本

本文閱讀的源碼爲 zepto1.2.0git

GitBook

reading-zeptogithub

實現的事件

;['swipe', 'swipeLeft', 'swipeRight', 'swipeUp', 'swipeDown',
  'doubleTap', 'tap', 'singleTap', 'longTap'].forEach(function(eventName){
  $.fn[eventName] = function(callback){ return this.on(eventName, callback) }
})

從上面的代碼中能夠看到,Zepto 實現瞭如下的事件:segmentfault

  • 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')
}

返回的是滑動的方法。微信

x1x軸 起點座標, x2x軸 終點座標, y1y軸 起點座標, y2y軸 終點座標。異步

這裏有多組三元表達式,首先對比的是 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
}

是否爲主觸點。

pointerTypetouch 而且 isPrimarytrue 時,才爲主觸點。 pointerType 可爲 touchpenmouse ,這裏只處理手指觸摸的狀況。

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 所觸發的事件,是從 touchpointer 或者 IE 的 guesture 事件中,根據不一樣狀況計算出來的。這些事件都綁定在 document 上。

IE Gesture 事件的處理

IE 的手勢使用,須要經歷三步:

  1. 建立手勢對象
  2. 指定目標元素
  3. 指定手勢識別時須要處理的指針
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 事件。

velocityXvelocityY 分別爲 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

這裏的判斷其實就是隻處理 PointerEventTouchEvent ,而且 TouchEventisPrimary 必須爲 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.x1touch.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 中能夠看到,若是觸發了 startlast 確定是存在的,可是若是觸發了長按事件,touch 對象會被清空,這時不會再觸發 tap 事件。

若是不是 swipe 事件,也不存在 last ,則只將 touch 清空,不觸發任何事件。

在最後會將 deltaXdeltaY 重置爲 0

觸發 tap 事件時,會在 event 中加了 cancelTouch 方法,外界能夠經過這個方法取消全部事件的執行。

這裏一樣用了 setTimeout 異步觸發事件。

doubleTap

if (touch.isDoubleTap) {
  if (touch.el) touch.el.trigger('doubleTap')
  touch = {}
}

這個 isDoubleTapstart 時肯定的,上面已經分析過了,在 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 方法時, 能夠將事件取消。

系列文章

  1. 讀Zepto源碼之代碼結構
  2. 讀Zepto源碼以內部方法
  3. 讀Zepto源碼之工具函數
  4. 讀Zepto源碼之神奇的$
  5. 讀Zepto源碼之集合操做
  6. 讀Zepto源碼之集合元素查找
  7. 讀Zepto源碼之操做DOM
  8. 讀Zepto源碼之樣式操做
  9. 讀Zepto源碼之屬性操做
  10. 讀Zepto源碼之Event模塊
  11. 讀Zepto源碼之IE模塊
  12. 讀Zepto源碼之Callbacks模塊
  13. 讀Zepto源碼之Deferred模塊
  14. 讀Zepto源碼之Ajax模塊
  15. 讀Zepto源碼之Assets模塊
  16. 讀Zepto源碼之Selector模塊

參考

License

署名-非商業性使用-禁止演繹 4.0 國際 (CC BY-NC-ND 4.0)

最後,全部文章都會同步發送到微信公衆號上,歡迎關注,歡迎提意見:

做者:對角另外一面

相關文章
相關標籤/搜索