zoukankan      html  css  js  c++  java
  • 读Zepto源码之Touch模块

    大家都知道,因为历史原因,移动端上的点击事件会有 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 的手势使用,需要经历三步:

    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)

    作者:对角另一面

  • 相关阅读:
    Git在商业项目中的使用流程
    EventBus中观察者模式的应用
    作业三——安卓系统文件助手APP原型设计
    视频剪辑软件的调研——万兴神剪手、视频编辑王、爱剪辑
    18-10-31 Scrum Meeting 3
    myapp——自动生成小学四则运算题目的命令行程序(侯国鑫 谢嘉帆)
    一个「学渣」从零开始的Web前端自学之路
    Vue一个案例引发「内容分发slot」的最全总结
    Vue CLI 3.0脚手架如何在本地配置mock数据
    Vue一个案例引发「动画」的使用总结
  • 原文地址:https://www.cnblogs.com/hefty/p/7559611.html
Copyright © 2011-2022 走看看