7

所谓 zepto 的 touch 其实就是指这个文件啦,可以看到区区 165 行(包括注释)就完成了 swipe 和 tap 相关的事件实现。在正式开始分析源码之前,我们先说说 touch 相关的几个事件,因为无论是 tap 还是 swipe 都是基于他们的。

touch 相关事件

  1. touchstart 触摸屏幕的瞬间

  2. touchmove 手指在屏幕上的移动过程一直触发

  3. touchend 离开屏幕的瞬间

  4. touchcancel 触摸取消(取决于浏览器实现,并不常用)

触摸屏下事件触发顺序是

touchstart -> touchmove -> touchend -> click

引入 touch 的背景

click事件在移动端上会有 300ms 的延迟,同时因为需要 长按双触击 等富交互,所以我们通常都会引入类似 zepto 这样的库。zepto 实现了'swipe', 'swipeLeft', 'swipeRight', 'swipeUp', 'swipeDown', 'doubleTap', 'tap', 'singleTap', 'longTap' 这样一些功能。

zepto touch 源码

我们直接看到 touch 源码的 49 行,从这里开始就是上述事件的实现了。不难想到 MSGesture 是对 mobile ie 的实现,本文不做讨论。往下面看到 66 行,$(document).on('touchstart MSPointerDown pointerdown') 开始。

//判断事件类型是否为 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 依然会不变,但不会冒泡。最佳实践是将触摸事件的监听器绑定到这个元素本身, 防止元素被移除后, 无法再从它的上一级元素上侦测到从该元素冒泡的事件。

希望本文能解答一些大家在移动端开发当中的一些问题,本文行文匆忙如有不正确的地方希望能回复告知。


flashback
515 声望26 粉丝