Touch module reading Zepto source code

As we all know, for historical reasons, click events on the mobile end will have a delay of about 300 ms. Zepto's touch module solves the problem of click delay on the mobile end, and also provides sliding swipe events.

Read the Zepto source series on github. Welcome star: reading-zepto

Source version

The source code for this article is zepto1.2.0

GitBook

<reading-zepto>

Implemented events

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

As you can see from the above code, Zepto implements the following events:

  • swipe: Sliding events
  • swipeLeft: Left Slide Event
  • swipeRight: Slide to the right
  • swipeUp: Slide Up Event
  • swipeDown: Downward sliding event
  • Double Tap: Screen Double-click Event
  • tap: Screen click events respond faster than click events
  • singleTap: Screen click event
  • Long Tap: Long Press Event

And a shortcut is registered for each event.

Internal method

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')
}

Returns a sliding method.

x1 is the starting point coordinate of x axis, x2 is the end point coordinate of x axis, y1 is the starting point coordinate of y axis, y2 is the end point coordinate of y axis.

There are many sets of ternary expressions. First, the sliding distance on the x-axis and the y-axis is compared. If the sliding distance on the x-axis is larger than that on the y-axis, it will slide left and right, otherwise it will slide up and down.

On the x-axis, if the starting point is larger than the end point, slide to the left, return to Left, otherwise slide to the right, return to Right.

On the y-axis, if the starting point is larger than the end point, it slides upward and returns to Up, otherwise it slides downward and returns to Down.

longTap

var touch = {},
    touchTimeout, tapTimeout, swipeTimeout, longTapTimeout,
    longTapDelay = 750,
    gesture
function longTap() {
  longTapTimeout = null
  if (touch.last) {
    touch.el.trigger('longTap')
    touch = {}
  }
}

Trigger the long press event.

The touch object stores the information in the process of touching.

Before triggering the longTap event, the variable longTapTimeout that saves the timer is released. If there is last in the touch object, the longTap event is triggered. Last saves the last touch time. Finally, reset touch to an empty object for next use.

cancelLongTap

function cancelLongTap() {
  if (longTapTimeout) clearTimeout(longTapTimeout)
  longTapTimeout = null
}

Untrigger the longTap event.

If there is a timer that triggers the longTap, clearing the timer prevents the triggering of the longTap event.

Finally, you need to set the longTapTimeout variable to null and wait for garbage collection.

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 = {}
}

Clear the execution of all events.

In fact, it is to clear all the relevant timers, and finally set the touch object to null.

isPrimaryTouch

function isPrimaryTouch(event){
  return (event.pointerType == 'touch' ||
          event.pointerType == event.MSPOINTER_TYPE_TOUCH)
  && event.isPrimary
}

Is it the main contact?

The main contact is when the pointerType is touch and isPrimary is true. The pointerType can be touch, pen, and mouse, where only finger touch is handled.

isPointerEventType

function isPointerEventType(e, type){
  return (e.type == 'pointer'+type ||
          e.type.toLowerCase() == 'mspointer'+type)
}

Is the pointerEvent triggered?

In the low version of mobile IE browser, only PointerEvent is implemented, but TouchEvent is not implemented, so this judgment is needed.

Event triggering

Holistic analysis

$(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)

First, several variables are explained. now is used to save the current time, delta is used to save the time difference between two touches, deltaX is used to save the displacement on the x axis, deltaY is used to save the displacement on the y axis, first Touch is used to save the information of the initial touchpoint, and _isPointerType is used to save the judgment result of pointerEvent.

As you can see from the above, Zepto triggers events from touch, pointer or IE guesture events, calculated according to different circumstances. These events are bound to document s.

Processing of IE Gesture Events

IE's gesture use needs three steps:

  1. Create gesture objects
  2. Specify target elements
  3. Specifies the pointer to be processed for gesture recognition
if ('MSGesture' in window) {
  gesture = new MSGesture()
  gesture.target = document.body
}

This code contains the first two steps.

on('touchstart MSPointerDown pointerdown', function(e){
  ...
  if (gesture && _isPointerType) gesture.addPointer(e.pointerId)
}

This is the third step, using the addPointer method to specify the pointer to be processed.

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

The next step is to analyze gestures, which only deal with swipe events in Gesture.

velocityX and velocityY are the rates on the x-axis and y-axis, respectively. Here we use 1 or - 1 as the critical point to judge the direction of swipe.

If the direction of swipe exists, the swipe event is triggered, and the directed swipe event is also triggered.

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

Filter out non-touch events

if((_isPointerType = isPointerEventType(e, 'down')) &&
   !isPrimaryTouch(e)) return
firstTouch = _isPointerType ? e : e.touches[0]

The judgment result of isPointerEventType is also saved in _isPointerType to determine whether it is a PointerEvent or not.

The judgement here is that only PointerEvent and TouchEvent are processed, and TouchEvent's isPrimary must be true.

Because TouchEvent supports multiple touches, only the first point of the touch is stored in the first Touch variable.

Reset terminal coordinates

if (e.touches && e.touches.length === 1 && touch.x2) {
  touch.x2 = undefined
  touch.y2 = undefined
}

If records are needed, the endpoint coordinates need to be updated.

Normally, the touch object will be empty when touchEnd or cancel is touched, but if the user calls preventDefault, etc., there may be no empty case.

It's not clear why touches are emptied only when they are operated on a single point. Does it not need to be cleared when there are multiple contact points?

Record contact information

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 is used to save the current time.

delta is used to save the time interval between two clicks and to handle double-click events.

touch.el is used to save the target element. Here's a judgment. If the target is not a label node, take the parent node as the target element. This will occur when you click on the pseudo-Class element.

If touchTimeout exists, the timer is cleared to avoid repeated triggers.

touch.x1 and touch.y1 save X-axis coordinates and y-axis coordinates respectively.

dblclick

if (delta > 0 && delta <= 250) touch.isDoubleTap = true

It's clear that Zepto sets isDoubleTap to true when the time interval between two clicks is less than 250ms as a doubleTap event.

Long press event

touch.last = now
longTapTimeout = setTimeout(longTap, longTapDelay)

Set touch.last to the current time. This allows you to record the time difference between two clicks.

At the same time, the long press event timer starts. As can be seen from the above code, the long press event will trigger after 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)
})

The move event handles two things, one is to record the coordinates of the endpoint, and the other is to calculate the displacement between the starting point and the endpoint.

Note that cancelLongTap is also called here to clear the long press timer and avoid triggering the long press event. Because of the movement, it's definitely not a long press.

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)

When you enter the end, clear the execution of the longTap timer immediately.

It can be seen that when the distance between the starting point and the end point exceeds 30, it will be judged as a swipe sliding event.

After the swipe event is triggered, the swipe event in the corresponding direction is triggered immediately.

Note that the swipe event is not triggered immediately when the end series of events trigger, but set up a timer of 0ms to trigger the event asynchronously. What's the use of this? I'll talk about it later.

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

Finally, we can see the key point. First, we can judge whether last exists. From the start, we can see that if the start is triggered, last certainly exists, but if the long press event is triggered, the touch object will be empty, and then the tap event will not be triggered again.

If it's not a swipe event, and there's no last, just empty the touch and don't trigger anything.

In the end, deltaX and deltaY will be reset to 0.

When the tap event is triggered, cancelTouch method is added to the event, which can be used by the outside world to cancel the execution of all events.

setTimeout is also used to trigger events asynchronously.

doubleTap

if (touch.isDoubleTap) {
  if (touch.el) touch.el.trigger('doubleTap')
  touch = {}
}

This isDoubleTap is determined at start, which has been analyzed above, and triggers the doubleTap event at end.

Therefore, you can know that two tap events are triggered before the doubleTap event is triggered.

singleTap

touchTimeout = setTimeout(function(){
  touchTimeout = null
  if (touch.el) touch.el.trigger('singleTap')
  touch = {}
}, 250)

If it is not doubleTap, the singleTap event will be triggered 250ms after the tap event triggers.

cancel

.on('touchcancel MSPointerCancel pointercancel', cancelAll)

When a cancel event is received, the cancelAll method is called to cancel the triggering of all events.

scroll

$(window).on('scroll', cancelAll)

As can be seen from the previous analysis, all event triggers are asynchronous.

Because when scroll is used, you want to respond only to scrolling events, and asynchronous triggering is used to cancel events when cancelTouch method is called in the scroll process and outside.

Serial articles

  1. Code structure for reading Zepto source code
  2. Internal method of reading Zepto source code
  3. Tool functions for reading Zepto source code
  4. Reading the Magic of Zepto Source Code$
  5. Collection operations for reading Zepto source code
  6. Reading Zepto Source Collection Element Search
  7. DOM for reading Zepto source code
  8. Style operations for reading Zepto source code
  9. Property operations for reading Zepto source code
  10. Event module reading Zepto source code
  11. IE Module for Reading Zepto Source Code
  12. Callbacks module reading Zepto source code
  13. Deferred Module for Reading Zepto Source Code
  14. Ajax Module for Reading Zepto Source Code
  15. Assets module reading Zepto source code
  16. Selector module for reading Zepto source code

Reference resources

License

Signature - Noncommercial Use - Prohibition of Deduction 4.0 International (CC BY-NC-ND 4.0)

Finally, all articles will be sent to the Wechat Public Number synchronously. Welcome your attention and comments.

Author: Diagonally on the other side

Keywords: IE Mobile github less

Added by xiao on Thu, 23 May 2019 20:50:18 +0300