资讯专栏INFORMATION COLUMN

zepto touch 库源码分析

lentrue / 915人阅读

摘要:源码分析不愿意下代码的可以直接点这里地址首先赞一下的代码注释,非常全。属性一个对象,包含了代表所有从上一次触摸事件到此次事件过程中,状态发生了改变的触点的对象。

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

touch 相关事件

touchstart 触摸屏幕的瞬间

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

touchend 离开屏幕的瞬间

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 是一类描述手指在触摸平面(触摸屏、触摸板等)的状态变化的事件。这类事件用于描述一个或多个触点,使开发者可以检测触点的移动,触点的增加和减少,等等。

属性:

TouchEvent.changedTouches 一个 TouchList 对象,包含了代表所有从上一次触摸事件到此次事件过程中,状态发生了改变的触点的 Touch 对象。

TouchEvent.targetTouches 一个 TouchList 对象,是包含了如下触点的 Touch 对象:触摸起始于当前事件的目标 element 上,并且仍然没有离开触摸平面的触点.

TouchEvent.touches 一个 TouchList 对象,包含了所有当前接触触摸平面的触点的 Touch 对象,无论它们的起始于哪个 element 上,也无论它们状态是否发生了变化。

TouchEvent.type 此次触摸事件的类型,可能值为 touchstart, touchmove, touchend 等等

TouchEvent.target 触摸事件的目标 element,这个目标元素对应 TouchEvent.changedTouches 中的触点的起始元素。

TouchEvent.altKey, TouchEvent.ctrlKey, TouchEvent.metaKey, TouchEvent.shiftKey 触摸事件触发时,键盘对应的键(例如 alt )是否被按下。

TouchList 与 Touch

TouchList 就是一系列的 Touch,通过 TouchList.length 可以知道当前有几个触点,TouchList[0] 或者 TouchList.item(0) 用来访问第一个触点。

属性

Touch.identifier:touch 的唯一标志,整个 touch 过程中(也就是 end 之前)不会改变

Touch.screenXTouch.screenY:坐标原点为屏幕左上角

Touch.clientXTouch.clientY:坐标原点在当前可视区域左上角,这两个值不包含滚动偏移

Touch.pageXTouch.pageY:坐标原点在HTML文档左上角,这两个值包含了水平滚动的偏移

Touch.radiusXTouch.radiusY:触摸平面的最小椭圆的水平轴(X轴)半径和垂直轴(Y轴)半径

Touch.rotationAngle:触摸平面的最小椭圆与水平轴顺时针夹角

Touch.force:压力值 0.0-1.0

Touch.target:Touch相关事件触发时的 element 不会随 move 变化。如果 move 当中该元素被删掉,这个 target 依然会不变,但不会冒泡。最佳实践是将触摸事件的监听器绑定到这个元素本身, 防止元素被移除后, 无法再从它的上一级元素上侦测到从该元素冒泡的事件。

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/79895.html

相关文章

  • Zepto源码Touch模块

    摘要:在触发事件前,先将保存定时器的变量释放,如果对象中存在,则触发事件,保存的是最后触摸的时间。如果有触发的定时器,清除定时器即可阻止事件的触发。其实就是清除所有相关的定时器,最后将对象设置为。进入时,立刻清除定时器的执行。 大家都知道,因为历史原因,移动端上的点击事件会有 300ms 左右的延迟,Zepto 的 touch 模块解决的就是移动端点击延迟的问题,同时也提供了滑动的 swip...

    Prasanta 评论0 收藏0
  • Zepto源码之Gesture模块

    摘要:模块基于上的事件的封装,利用属性,封装出系列事件。这个判断需要引入设备侦测模块。然后是监测事件,根据这三个事件,可以组合出和事件。其中变量对象和模块中的对象的作用差不多,可以先看看读源码之模块对模块的分析。 Gesture 模块基于 IOS 上的 Gesture 事件的封装,利用 scale 属性,封装出 pinch 系列事件。 读 Zepto 源码系列文章已经放到了github上,欢...

    coolpail 评论0 收藏0
  • FastClick 源码解读

    摘要:所有浏览器浏览器不支持安卓中中有属性安卓中中有属性有属性的有属性的所以在不需要的浏览器会直接掉,不会执行下面的所有代码。见源码行,可以看出在响应无操作后,则触发。 其实一直就想花些时间读一读那些优秀的开源库,今天终于下了决定打算死磕下自己,2016年每个月读2-3个优秀的开源库,把源码精彩的地方和自己心得分享给大家。 目录 (一)背景(二)源码解析(三)Zepto 点击穿透与 Fast...

    Chaz 评论0 收藏0
  • 如何实现swipe、tap、longTap等自定义事件

    摘要:分别存储事件的定时器。事件定时器延时时间存储事件对象滑动方向判断我们根据下图以及对应的代码来理解滑动的时候方向是如何判定的。取消长按,以及取消所有事件取消长按取消所有事件方式都是类似,先调用取消定时器,然后释放对应的变量,等候垃圾回收。 前言 移动端原生支持touchstart、touchmove、touchend等事件,但是在平常业务中我们经常需要使用swipe、tap、double...

    罗志环 评论0 收藏0

发表评论

0条评论

lentrue

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<