您的位置:首页 > Web前端 > JQuery

读Zepto源码之Touch模块

2017-09-20 07:41 495 查看
大家都知道,因为历史原因,移动端上的点击事件会有
300ms
左右的延迟,
Zepto
touch
模块解决的就是移动端点击延迟的问题,同时也提供了滑动的
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')
}


返回的是滑动的方法。

x1
x轴
起点坐标,
x2
x轴
终点坐标,
y1
y轴
起点坐标,
y2
y轴
终点坐标。

这里有多组三元表达式,首先对比的是
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
}


是否为主触点。

pointerType
touch
并且
isPrimary
true
时,才为主触点。
pointerType
可为
touch
pen
mouse
,这里只处理手指触摸的情况。

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
所触发的事件,是从
touch
pointer
或者 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
事件。

velocityX
velocityY
分别为
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


这里的判断其实就是只处理
PointerEvent
TouchEvent
,并且
TouchEvent
isPrimary
必须为
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.x1
touch.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
中可以看到,如果触发了
start
last
肯定是存在的,但是如果触发了长按事件,
touch
对象会被清空,这时不会再触发
tap
事件。

如果不是
swipe
事件,也不存在
last
,则只将
touch
清空,不触发任何事件。

在最后会将
deltaX
deltaY
重置为
0


触发
tap
事件时,会在
event
中加了
cancelTouch
方法,外界可以通过这个方法取消所有事件的执行。

这里同样用了
setTimeout
异步触发事件。

doubleTap

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


这个
isDoubleTap
start
时确定的,上面已经分析过了,在
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

License

署名-非商业性使用-禁止演绎 4.0 国际 (CC BY-NC-ND 4.0)

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


作者:对角另一面
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息