zepto touch 庫源碼分析

所謂 zepto 的 touch 其實就是指這個文件啦,能夠看到區區 165 行(包括註釋)就完成了 swipe 和 tap 相關的事件實現。在正式開始分析源碼以前,咱們先說說 touch 相關的幾個事件,由於不管是 tap 仍是 swipe 都是基於他們的。javascript

touch 相關事件

  1. touchstart 觸摸屏幕的瞬間java

  2. touchmove 手指在屏幕上的移動過程一直觸發git

  3. touchend 離開屏幕的瞬間github

  4. touchcancel 觸摸取消(取決於瀏覽器實現,並不經常使用)瀏覽器

觸摸屏下事件觸發順序是app

touchstart -> touchmove -> touchend -> click

引入 touch 的背景

click事件在移動端上會有 300ms 的延遲,同時由於須要 長按雙觸擊 等富交互,因此咱們一般都會引入相似 zepto 這樣的庫。zepto 實現了'swipe', 'swipeLeft', 'swipeRight', 'swipeUp', 'swipeDown', 'doubleTap', 'tap', 'singleTap', 'longTap' 這樣一些功能。dom

zepto touch 源碼

咱們直接看到 touch 源碼的 49 行,從這裏開始就是上述事件的實現了。不難想到 MSGesture 是對 mobile ie 的實現,本文不作討論。往下面看到 66 行,$(document).on('touchstart MSPointerDown pointerdown') 開始。ide

//判斷事件類型是否爲 touch
if((_isPointerType = isPointerEventType(e, 'down')) &&
  !isPrimaryTouch(e)) return
// touches 是觸摸點的數量
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 事件的 dom 
touch.el = $('tagName' in firstTouch.target ?
  firstTouch.target : firstTouch.target.parentNode)
// 若是 touchTimeout 存在就清理掉
touchTimeout && clearTimeout(touchTimeout)
// 記錄當前座標
touch.x1 = firstTouch.pageX
touch.y1 = firstTouch.pageY
// 觸摸時間差小於 250ms 則爲 DoubleTap
if (delta > 0 && delta <= 250) touch.isDoubleTap = true
// 記錄執行後的時間
touch.last = now
// 留一個長觸摸,若是 touchmove 會把這個清理掉
longTapTimeout = setTimeout(longTap, longTapDelay)

接下來是 $(document).on('touchmove MSPointerMove pointermove')函數

//判斷事件類型是否爲 move
if((_isPointerType = isPointerEventType(e, 'move')) &&
          !isPrimaryTouch(e)) return
firstTouch = _isPointerType ? e : e.touches[0]
// 一旦進入 move 就會清理掉 LongTap
cancelLongTap()
// 當前手指座標
touch.x2 = firstTouch.pageX
touch.y2 = firstTouch.pageY
// x 軸和 y 軸的變化量 Math.abs 是取絕對值的意思
deltaX += Math.abs(touch.x1 - touch.x2)
deltaY += Math.abs(touch.y1 - touch.y2)

最後固然就是 $(document).on('touchend MSPointerUp pointerup') 了,這個也是整個 touch 最爲複雜的一部分。源碼分析

if((_isPointerType = isPointerEventType(e, 'up')) &&
          !isPrimaryTouch(e)) return
        cancelLongTap()

    // 若是是 swipe,x 軸或者 y 軸移動超過 30px
    if ((touch.x2 && Math.abs(touch.x1 - touch.x2) > 30) ||
        (touch.y2 && Math.abs(touch.y1 - touch.y2) > 30))

      swipeTimeout = setTimeout(function() {
        touch.el.trigger('swipe')
        // swipeDirection 是判斷 swipe 方向的
        touch.el.trigger('swipe' + (swipeDirection(touch.x1, touch.x2, touch.y1, touch.y2)))
        touch = {}
      }, 0)

    // tap 事件
    else if ('last' in touch)
      if (deltaX < 30 && deltaY < 30) {
         // tapTimeout 是爲了 scroll 的時候方便清除
        tapTimeout = setTimeout(function() {
          // 建立 tap 事件,並增長 cancelTouch 方法
          var event = $.Event('tap')
          event.cancelTouch = cancelAll
          touch.el.trigger(event)

          // 觸發 DoubleTap
          if (touch.isDoubleTap) {
            if (touch.el) touch.el.trigger('doubleTap')
            touch = {}
          }

          // singleTap (這個概念是相對於 DoubleTap 的,能夠看看咱們在最初的那段源碼解析中有這樣一段 if (delta > 0 && delta <= 250) touch.isDoubleTap = true ,因此 250 ms 以後沒有二次觸摸的就算是 singleTap 了 
          else {
            touchTimeout = setTimeout(function(){
              touchTimeout = null
              if (touch.el) touch.el.trigger('singleTap')
              touch = {}
            }, 250)
          }
        }, 0)
      } else {
        touch = {}
      }
      deltaX = deltaY = 0

整個讀下來其實就是對 touchstart, touchmove, touchend 作了一些封裝和判斷,而後經過 zepto 本身的事件體系來註冊和觸發。

fastclick 對比 zepto

咱們在聊到移動端 js 方案的時候很容易聽到這二者,但我我的認爲這二者是沒法對比的。緣由以下:zepto 是一個移動端的 js 庫,而 fastclick 專一於 click 在移動端的觸發問題。fastclick 的 github 主頁上有一句話是「Polyfill to remove click delays on browsers with touch UIs」,翻譯過來就是幹掉移動端 click 延時的補丁。這個延時就是咱們在引入 touch 的背景裏提到過。

fastclick 源碼分析

不肯意下代碼的能夠直接點這裏github地址首先贊一下 fastclick 的代碼註釋,很是全。

fastclick 的使用很是簡單,直接 FastClick.attach(document.body); 一句話搞定。因此源碼分析就從 attach 方法來看吧,824 行

FastClick.attach = function(layer, options) {
        // 返回 FastClick 實例 layer 是一個 element 一般是 document.body ,options 天然就是配置了
        return new FastClick(layer, options);
    };

接下來回到 23 行看到 FastClick 構造函數,

// 方法綁定,兼容老版本的安卓
function bind(method, context) {
    return function() { return method.apply(context, arguments); };
}

var methods = ['onMouse', 'onClick', 'onTouchStart', 'onTouchMove', 'onTouchEnd', 'onTouchCancel'];
var context = this;
for (var i = 0, l = methods.length; i < l; i++) {
    context[methods[i]] = bind(context[methods[i]], context);
}
 // 事件處理綁定部分
if (deviceIsAndroid) {
    layer.addEventListener('mouseover', this.onMouse, true);
    layer.addEventListener('mousedown', this.onMouse, true);
    layer.addEventListener('mouseup', this.onMouse, true);
}

layer.addEventListener('click', this.onClick, true);
layer.addEventListener('touchstart', this.onTouchStart, false);
layer.addEventListener('touchmove', this.onTouchMove, false);
layer.addEventListener('touchend', this.onTouchEnd, false);
layer.addEventListener('touchcancel', this.onTouchCancel, false);

 // stopImmediatePropagation 的兼容
 
 if (!Event.prototype.stopImmediatePropagation) {
    layer.removeEventListener = function(type, callback, capture) {
        var rmv = Node.prototype.removeEventListener;
        if (type === 'click') {
            rmv.call(layer, type, callback.hijacked || callback, capture);
        } else {
            rmv.call(layer, type, callback, capture);
        }
    };

    layer.addEventListener = function(type, callback, capture) {
        var adv = Node.prototype.addEventListener;
        if (type === 'click') {
            adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) {
                if (!event.propagationStopped) {
                    callback(event);
                }
            }), capture);
        } else {
            adv.call(layer, type, callback, capture);
        }
    };
}

// 若是 layer 有 onclick ,就把 onclick 轉換爲 addEventListener 的方式
if (typeof layer.onclick === 'function') {
    oldOnClick = layer.onclick;
    layer.addEventListener('click', function(event) {
        oldOnClick(event);
    }, false);
    layer.onclick = null;
}

FastClick.prototype.onTouchStart 和 zepto 同樣作了一些參數的紀錄,因此我這裏就直接跳到 FastClick.prototype.onTouchEnd 看 fastclick 的核心。

FastClick.prototype.onTouchEnd = function(event) {
    var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement;

    if (!this.trackingClick) {
        return true;
    }
    // 防止 double tap 的時間間隔內 click 觸發
    if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {
        this.cancelNextClick = true;
        return true;
    }
    // 超出 longtap 的時間
    if ((event.timeStamp - this.trackingClickStart) > this.tapTimeout) {
        return true;
    }

    this.cancelNextClick = false;
    // 紀錄當前時間
    this.lastClickTime = event.timeStamp;

    trackingClickStart = this.trackingClickStart;
    this.trackingClick = false;
    this.trackingClickStart = 0;

    if (deviceIsIOSWithBadTarget) {
        touch = event.changedTouches[0];
        targetElement = document.elementFromPoint(touch.pageX - window.pageXOffset, touch.pageY - window.pageYOffset) || targetElement;
        targetElement.fastClickScrollParent = this.targetElement.fastClickScrollParent;
    }
    // 獲取 targetTagName 上面的一段是 targetTagName 兼容性
    targetTagName = targetElement.tagName.toLowerCase();
    // 解決 label for
    if (targetTagName === 'label') {
        forElement = this.findControl(targetElement);
        if (forElement) {
            this.focus(targetElement);
            if (deviceIsAndroid) {
                return false;
            }

            targetElement = forElement;
        }
    } else if (this.needsFocus(targetElement)) {
        if ((event.timeStamp - trackingClickStart) > 100 || (deviceIsIOS && window.top !== window && targetTagName === 'input')) {
            this.targetElement = null;
            return false;
        }
        // 解決 input focus 
        this.focus(targetElement);
        // 觸發 sendClick
        this.sendClick(targetElement, event);

        if (!deviceIsIOS || targetTagName !== 'select') {
            this.targetElement = null;
            event.preventDefault();
        }

        return false;
    }

    if (deviceIsIOS && !deviceIsIOS4) {
        scrollParent = targetElement.fastClickScrollParent;
        if (scrollParent && scrollParent.fastClickLastScrollTop !== scrollParent.scrollTop) {
            return true;
        }
    }
    // 最後就來觸發 sendClick 了
    if (!this.needsClick(targetElement)) {
        event.preventDefault();
        this.sendClick(targetElement, event);
    }

    return false;
};

看完上面的代碼,咱們立刻來解讀 FastClick.prototype.sendClick

FastClick.prototype.sendClick = function(targetElement, event) {
    var clickEvent, touch;
    // 拿觸摸的第一個手指
    touch = event.changedTouches[0];
    // 自定義 clickEvent 事件
    clickEvent = document.createEvent('MouseEvents');
    clickEvent.initMouseEvent(this.determineEventType(targetElement), true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);
    clickEvent.forwardedTouchEvent = true;
    // 觸發 clickEvent 事件
    targetElement.dispatchEvent(clickEvent);
};

到此 fastclick 主要的東西咱們就看得差很少了,代碼當中不難看到 fastclick 的兼容性作的很好。它的主要目的是解決 click 在觸摸屏下的使用,引入以後再初始化一次就行了,很適合複用代碼的情景。

擴展講一下 touchEvent

本文中 zepto 和 fastclick 都有用到 touchEvent,可是 zepto 當中用的是 e.touches 而 fastclick 卻用的是 e.targetTouches。這二者的差別咱們來一點一點地扒。

TouchEvent 是一類描述手指在觸摸平面(觸摸屏、觸摸板等)的狀態變化的事件。這類事件用於描述一個或多個觸點,使開發者能夠檢測觸點的移動,觸點的增長和減小,等等。

屬性:

  1. TouchEvent.changedTouches 一個 TouchList 對象,包含了表明全部從上一次觸摸事件到這次事件過程當中,狀態發生了改變的觸點的 Touch 對象。

  2. TouchEvent.targetTouches 一個 TouchList 對象,是包含了以下觸點的 Touch 對象:觸摸起始於當前事件的目標 element 上,而且仍然沒有離開觸摸平面的觸點.

  3. TouchEvent.touches 一個 TouchList 對象,包含了全部當前接觸觸摸平面的觸點的 Touch 對象,不管它們的起始於哪一個 element 上,也不管它們狀態是否發生了變化。

  4. TouchEvent.type 這次觸摸事件的類型,可能值爲 touchstart, touchmove, touchend 等等

  5. TouchEvent.target 觸摸事件的目標 element,這個目標元素對應 TouchEvent.changedTouches 中的觸點的起始元素。

  6. TouchEvent.altKey, TouchEvent.ctrlKey, TouchEvent.metaKey, TouchEvent.shiftKey 觸摸事件觸發時,鍵盤對應的鍵(例如 alt )是否被按下。

TouchList 與 Touch

TouchList 就是一系列的 Touch,經過 TouchList.length 能夠知道當前有幾個觸點,TouchList[0] 或者 TouchList.item(0) 用來訪問第一個觸點。

屬性

  1. Touch.identifier:touch 的惟一標誌,整個 touch 過程當中(也就是 end 以前)不會改變

  2. Touch.screenXTouch.screenY:座標原點爲屏幕左上角

  3. Touch.clientXTouch.clientY:座標原點在當前可視區域左上角,這兩個值不包含滾動偏移

  4. Touch.pageXTouch.pageY:座標原點在HTML文檔左上角,這兩個值包含了水平滾動的偏移

  5. Touch.radiusXTouch.radiusY:觸摸平面的最小橢圓的水平軸(X軸)半徑和垂直軸(Y軸)半徑

  6. Touch.rotationAngle:觸摸平面的最小橢圓與水平軸順時針夾角

  7. Touch.force:壓力值 0.0-1.0

  8. Touch.target:Touch相關事件觸發時的 element 不會隨 move 變化。若是 move 當中該元素被刪掉,這個 target 依然會不變,但不會冒泡。最佳實踐是將觸摸事件的監聽器綁定到這個元素自己, 防止元素被移除後, 沒法再從它的上一級元素上偵測到從該元素冒泡的事件。

但願本文能解答一些你們在移動端開發當中的一些問題,本文行文匆忙若有不正確的地方但願能回覆告知。

相關文章
相關標籤/搜索