zepto源码研究 - touch.js

自闭症网瘾萝莉.ら 提交于 2020-03-16 19:12:42

简要:touch.js 主要提供滑动(swipe)与点击(tap:模拟click)的事件封装,针对手机常用浏览器(touchstart,touchmove,touchend)和IE10(msPointDown)的触摸事件兼容处理以及手势的事件处理。之所以封装touchstart形成tap是因为要解决点透问题,并且模拟click有双击与长按功能。

源码分析如下:

//     Zepto.js
//     (c) 2010-2016 Thomas Fuchs
//     Zepto.js may be freely distributed under the MIT license.

;(function($){
  var touch = {},
    touchTimeout, tapTimeout, swipeTimeout, longTapTimeout,
    longTapDelay = 750,
    gesture

  //(x1,y1)为开始点,(x2,y2)为结束点,判断滑动的方向
  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')
  }

  //长按触发事件
  function longTap() {
    longTapTimeout = null
    if (touch.last) {
      touch.el.trigger('longTap')
      touch = {}
    }
  }

  //取消长按事件,在longTap未执行前,longTapTimeout计时器不为null
  function cancelLongTap() {
    if (longTapTimeout) clearTimeout(longTapTimeout)
    longTapTimeout = null
  }

  //清除所有类型的计时器,取消所有事件
  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 = {}
  }

  // 判断是否是第一个touch或pointer事件对象
  // 检测主触点
  // 参考链接:http://www.w3cplus.com/css3/adapting-your-webkit-optimized-site-for-internet-explorer-10.html
  //如前面所说,指针事件模型为每个触点触发单独的事件。因此,如果你只想处理主触点(比如单指拖动的情况),
  //你就需要在“move”和“up”处理之前使用下面这条语句过滤掉非主触点的触摸点:
  function isPrimaryTouch(event){
    return (event.pointerType == 'touch' ||
      event.pointerType == event.MSPOINTER_TYPE_TOUCH)
      && event.isPrimary
  }

  //判断是否为IE指针事件,参照http://www.ayqy.net/blog/html5%E8%A7%A6%E6%91%B8%E4%BA%8B%E4%BB%B6/
  function isPointerEventType(e, type){
    return (e.type == 'pointer'+type ||
      e.type.toLowerCase() == 'mspointer'+type)
  }


  $(document).ready(function(){
    var now, delta, deltaX = 0, deltaY = 0, firstTouch, _isPointerType

    //手势操作对象,在IE10中有效,参照http://www.jb51.net/html5/79175.html
    if ('MSGesture' in window) {
      gesture = new MSGesture()
      gesture.target = document.body
    }

    $(document)
       // 根据手势来判断滑动的方向,与后面根据距离来判断是不同的,
       // 参照链接:http://www.th7.cn/web/js/201309/12944.shtml
      .bind('MSGestureEnd', function(e){
        //e.velocityX,e.velocityY,判断手势的方向
        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)
        }
      })
      .on('touchstart MSPointerDown pointerdown', function(e){
        if((_isPointerType = isPointerEventType(e, 'down')) &&
          !isPrimaryTouch(e)) return
        //获取第一个触点对象,若是指针对象,则是他自己
        firstTouch = _isPointerType ? e : e.touches[0]

        //清除掉之前触摸移动的touch数据,原因是因某些事件阻止默认行为,
        // 导致touchcancel未被调用,touch数据未被清除
        // TODO 具体是什么样的行为,有待具体分析
        if (e.touches && e.touches.length === 1 && touch.x2) {
          // Clear out touch movement data if we have it sticking around
          // This can occur if touchcancel doesn't fire due to preventDefault, etc.
          touch.x2 = undefined
          touch.y2 = undefined
        }

        //获取当前时间点
        now = Date.now()
        //touch.last代表上一次触摸时间,delta标示两次触摸的间隔,用于判断是否为长按
        delta = now - (touch.last || now)

        //如果firstTouch.target是正常的dom元素,则赋值于touch.el,
        //若是非正常元素,比如伪元素,则touch源是其父元素
        touch.el = $('tagName' in firstTouch.target ?
          firstTouch.target : firstTouch.target.parentNode)

        //保证singleTouch计时器被清除
        touchTimeout && clearTimeout(touchTimeout)

        //获取touch点相对与page的位置
        touch.x1 = firstTouch.pageX
        touch.y1 = firstTouch.pageY

        //手指双击的判断,如果是轻触,则delta为0
        if (delta > 0 && delta <= 250) touch.isDoubleTap = true

        //待参数都处理完后,将本次触点时间作为上一个次触点时间
        touch.last = now

        //默认按照长按事件处理
        longTapTimeout = setTimeout(longTap, longTapDelay)

        // adds the current touch contact for IE gesture recognition
        if (gesture && _isPointerType) gesture.addPointer(e.pointerId);
      })
      .on('touchmove MSPointerMove pointermove', function(e){
        if((_isPointerType = isPointerEventType(e, 'move')) &&
          !isPrimaryTouch(e)) return
        firstTouch = _isPointerType ? e : e.touches[0]

        //取消长按,在touchstart的时候会设置定时器
        cancelLongTap()

        //设置touch对象的位置
        touch.x2 = firstTouch.pageX
        touch.y2 = firstTouch.pageY

        //获取滑动的距离
        deltaX += Math.abs(touch.x1 - touch.x2)
        deltaY += Math.abs(touch.y1 - touch.y2)
      })
      .on('touchend MSPointerUp pointerup', function(e){
        if((_isPointerType = isPointerEventType(e, 'up')) &&
          !isPrimaryTouch(e)) return
        cancelLongTap()

        // swipe,根据距离判断是否为有效滑动
        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');
            touch.el.trigger('swipe' + (swipeDirection(touch.x1, touch.x2, touch.y1, touch.y2)))
            touch = {}
          }, 0)

        // normal tap
        // 若touch为空对象,表示为长按,接下来赋空值
        else if ('last' in touch)
          // last in touch  表明longTap未执行
          // don't fire tap when delta position changed by more than 30 pixels,
          // for instance when moving to a point and back to origin
          if (deltaX < 30 && deltaY < 30) {
            // delay by one tick so we can cancel the 'tap' event if 'scroll' fires
            // ('tap' fires before 'scroll')
            // 手指移动距离短,可认为是轻触tap
            tapTimeout = setTimeout(function() {

              // trigger universal 'tap' with the option to cancelTouch()
              // (cancelTouch cancels processing of single vs double taps for faster 'tap' response)
              var event = $.Event('tap')
              event.cancelTouch = cancelAll
              touch.el.trigger(event)

              // trigger double tap immediately
              if (touch.isDoubleTap) {
                if (touch.el) touch.el.trigger('doubleTap')
                touch = {}
              }

              // trigger single tap after 250ms of inactivity
              else {
                touchTimeout = setTimeout(function(){
                  touchTimeout = null
                  if (touch.el) touch.el.trigger('singleTap')
                  touch = {}
                }, 250)
              }

            }, 0)
          } else {
            touch = {}
          }
          deltaX = deltaY = 0

      })
      // when the browser window loses focus,
      // for example when a modal dialog is shown,
      // cancel all ongoing events
      .on('touchcancel MSPointerCancel pointercancel', cancelAll)

      // scrolling the window indicates intention of the user
      // to scroll, not tap or swipe, so cancel all ongoing events
      $(window).on('scroll', cancelAll)
  })

  ;['swipe', 'swipeLeft', 'swipeRight', 'swipeUp', 'swipeDown',
    'doubleTap', 'tap', 'singleTap', 'longTap'].forEach(function(eventName){
    $.fn[eventName] = function(callback){ return this.on(eventName, callback) }
  })
})(Zepto)

我们先看一下其代码组织形式:

var touch = {},
    touchTimeout, tapTimeout, swipeTimeout, longTapTimeout,
    longTapDelay = 750,
    gesture

//(x1,y1)为开始点,(x2,y2)为结束点,判断滑动的方向
  function swipeDirection(x1, x2, y1, y2){}

//长按触发事件
  function longTap(){}

//取消长按事件,在longTap未执行前,longTapTimeout计时器不为null
  function cancelLongTap(){}

//清除所有类型的计时器,取消所有事件
  function cancelAll() {}

function isPrimaryTouch(event){}

//判断是否为IE指针事件,参照http://www.ayqy.net/blog/html5%E8%A7%A6%E6%91%B8%E4%BA%8B%E4%BB%B6/
function isPointerEventType(e, type){}


$(document).ready(function(){})

;['swipe', 'swipeLeft', 'swipeRight', 'swipeUp', 'swipeDown',
    'doubleTap', 'tap', 'singleTap', 'longTap'].forEach(function(eventName){
    $.fn[eventName] = function(callback){ return this.on(eventName, callback) }
  })

1:这里注册的事件都是自定义事件,回调与事件触发直接由zepto的event.js管理,这里,zepto将自定义事件的触发放在原生事件触发的回调函数中去,当用户点击屏幕的时候,会触发原生事件,而自定义的事件会在原生事件中被触发。

2:这里涉及到了一个技巧:那就是对于$.fn[eventName]事件注册,只是注册了自定义事件,而利用document来注册原生事件,但点击某个元素的时候,利用事件冒泡到document上,触发document上所注册的函数,在函数内部获取点击元素对象进而触发自定义事件。这样写既然代码重用了,又不必对每个元素都监听原生事件,减少了性能的开销,这是一个非常巧妙的方法。

3:还是一如既往的代码组织风格,先将工具尽量抽离出来,最后利用循环来统一来生产各个方法。

接下来看一下$(document).ready()里面的执行内容:

document的注册事件在文档加载完以后执行,这是为了保证文档加载完之前,只存储而不执行自定义事件。

MSGestrue:IE10中的手势对象,具体信息参考链接 : http://www.jb51.net/html5/79175.html,

接下来是document绑定MSGestrueEnd事件,判断手势的方向,进而触发滑动事件,关于如何运用MSGestrue,参考链接:http://www.th7.cn/web/js/201309/12944.shtml

这里主要介绍如何运用手势事件到元素的css上:

targetElement.addEventListener("MSGestureChange", manipulateElement);
function manipulateElement(e) {
// Uncomment the following code if you want to disable the built-in inertia provided by dynamic gesture recognition
// if (e.detail == e.MSGESTURE_FLAG_INERTIA)
// return;
var m = new MSCSSMatrix(e.target.style.transform); // Get the latest CSS transform on the element
e.target.style.transform = m
.translate(e.offsetX, e.offsetY) // Move the transform origin under the center of the gesture
.rotate(e.rotation * 180 / Math.PI) // Apply Rotation
.scale(e.scale) // Apply Scale
.translate(e.translationX, e.translationY) // Apply Translation
.translate(-e.offsetX, -e.offsetY); // Move the transform origin back
}

 

这里注册touchstart  MSPointerDown  pointerdown 事件,其大致功能是

1:判断是否为主触点,指针事件中每个触点都会触发事件,若不是则直接返回。2:获取有效触点对象,计算离上一次的触摸时间以便判断是否为双击

(双击的间隔时间一定比长按短)

3:获取事件源,默认作为长按处理。


接着注册touchmove  MSPointerMove  pointermove 事件,其大致功能是:

1:判断是否为主触点,指针事件中每个触点都会触发事件,若不是则直接返回。2:获取有效触点对象,取消长按定时器,3:计算滑动距离

 

最后注册touchend  MSPointerUp  pointerup 事件,其大致功能是:

1:判断是否为主触点,指针事件中每个触点都会触发事件,若不是则直接返回。2:取消长按定时器。3:根据滑动距离判断是否为有效滑动,若有效,则触发滑动事件

4:如果touch是空对象,则表示已经触发了长按事件。若没有,则表示在默认行为触发长按之前,手指离开屏幕,表明实际未发生长按行为。即发生了tap事件。触发tap的回调方法。但这里貌似未对点透事件做出处理(如果要处理,貌似要对事件做组织默认行为处理)。

那么如果在手指触摸或者滑动的过程中,失去了屏幕的焦点,或者用户滑动比较快,触发了滚动事件,则调用cancelAll,取消所有触摸行为。

// when the browser window loses focus,// for example when a modal dialog is shown,// cancel all ongoing events.on('touchcancel MSPointerCancel pointercancel', cancelAll)// scrolling the window indicates intention of the user// to scroll, not tap or swipe, so cancel all ongoing events$(window).on('scroll', cancelAll)

 

 

 

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!