资讯专栏INFORMATION COLUMN

读Zepto源码之Touch模块

Prasanta / 2807人阅读

摘要:在触发事件前,先将保存定时器的变量释放,如果对象中存在,则触发事件,保存的是最后触摸的时间。如果有触发的定时器,清除定时器即可阻止事件的触发。其实就是清除所有相关的定时器,最后将对象设置为。进入时,立刻清除定时器的执行。

大家都知道,因为历史原因,移动端上的点击事件会有 300ms 左右的延迟,Zeptotouch 模块解决的就是移动端点击延迟的问题,同时也提供了滑动的 swipe 事件。

读 Zepto 源码系列文章已经放到了github上,欢迎star: reading-zepto

源码版本

本文阅读的源码为 zepto1.2.0

GitBook

《reading-zepto》

实现的事件
;["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")
}

返回的是滑动的方法。

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 的手势使用,需要经历三步:

创建手势对象

指定目标元素

指定手势识别时需要处理的指针

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 方法时, 可以将事件取消。

系列文章

读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)

最后,所有文章都会同步发送到微信公众号上,欢迎关注,欢迎提意见:

作者:对角另一面

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

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

相关文章

  • Zepto源码Gesture模块

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

    coolpail 评论0 收藏0
  • Zepto源码Stack模块

    摘要:读源码系列文章已经放到了上,欢迎源码版本本文阅读的源码为改写原有的方法模块改写了以上这些方法,这些方法在调用的时候,会为返回的结果添加的属性,用来保存原来的集合。方法的分析可以看读源码之模块。 Stack 模块为 Zepto 添加了 addSelf 和 end 方法。 读 Zepto 源码系列文章已经放到了github上,欢迎star: reading-zepto 源码版本 本文阅读的...

    crossea 评论0 收藏0
  • Zepto源码Form模块

    摘要:模块处理的是表单提交。表单提交包含两部分,一部分是格式化表单数据,另一部分是触发事件,提交表单。最终返回的结果是一个数组,每个数组项为包含和属性的对象。否则手动绑定事件,如果没有阻止浏览器的默认事件,则在第一个表单上触发,提交表单。 Form 模块处理的是表单提交。表单提交包含两部分,一部分是格式化表单数据,另一部分是触发 submit 事件,提交表单。 读 Zepto 源码系列文章已...

    陈江龙 评论0 收藏0
  • Zepto源码fx_methods模块

    摘要:所以模块依赖于模块,在引入前必须引入模块。原有的方法分析见读源码之样式操作方法首先调用原有的方法,将元素显示出来,这是实现动画的基本条件。如果没有传递,或者为值,则表示不需要动画,调用原有的方法即可。 fx 模块提供了 animate 动画方法,fx_methods 利用 animate 方法,提供一些常用的动画方法。所以 fx_methods 模块依赖于 fx 模块,在引入 fx_m...

    junbaor 评论0 收藏0
  • Zepto源码IOS3模块

    摘要:用法与参数要理解这段代码,先来看一下的用法和参数用法参数回调函数,有如下参数上一个回调函数返回的值或者是初始值当前值当前值在数组中的索引调用的数组初始值,如果没有提供,则为数组的第一项。接下来,检测回调函数是否为,如果不是,抛出类型错误。 IOS3 模块是针对 IOS 的兼容模块,实现了两个常用方法的兼容,这两个方法分别是 trim 和 reduce 。 读 Zepto 源码系列文章...

    lavnFan 评论0 收藏0

发表评论

0条评论

Prasanta

|高级讲师

TA的文章

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