读Zepto源码之Touch模块
大家都知道,因为历史原因,移动端上的点击事件会有 300ms
左右的延迟,Zepto
的 touch
模块解决的就是移动端点击延迟的问题,同时也提供了滑动的 swipe
事件。
读 Zepto 源码系列文章已经放到了github上,欢迎star: reading-zepto
源码版本
本文阅读的源码为 zepto1.2.0
GitBook
实现的事件
;['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)
作者:对角另一面
读Zepto源码之Touch模块的更多相关文章
- 读Zepto源码之Gesture模块
Gesture 模块基于 IOS 上的 Gesture 事件的封装,利用 scale 属性,封装出 pinch 系列事件. 读 Zepto 源码系列文章已经放到了github上,欢迎star: rea ...
- 读Zepto源码之IOS3模块
IOS3 模块是针对 IOS 的兼容模块,实现了两个常用方法的兼容,这两个方法分别是 trim 和 reduce . 读 Zepto 源码系列文章已经放到了github上,欢迎star: readin ...
- 读Zepto源码之Fx模块
fx 模块为利用 CSS3 的过渡和动画的属性为 Zepto 提供了动画的功能,在 fx 模块中,只做了事件和样式浏览器前缀的补全,没有做太多的兼容.对于不支持 CSS3 过渡和动画的, Zepto ...
- 读Zepto源码之fx_methods模块
fx 模块提供了 animate 动画方法,fx_methods 利用 animate 方法,提供一些常用的动画方法.所以 fx_methods 模块依赖于 fx 模块,在引入 fx_methods ...
- 读Zepto源码之Stack模块
Stack 模块为 Zepto 添加了 addSelf 和 end 方法. 读 Zepto 源码系列文章已经放到了github上,欢迎star: reading-zepto 源码版本 本文阅读的源码为 ...
- 读Zepto源码之Form模块
Form 模块处理的是表单提交.表单提交包含两部分,一部分是格式化表单数据,另一部分是触发 submit 事件,提交表单. 读 Zepto 源码系列文章已经放到了github上,欢迎star: rea ...
- 读Zepto源码之Data模块
Zepto 的 Data 模块用来获取 DOM 节点中的 data-* 属性的数据,和储存跟 DOM 相关的数据. 读 Zepto 源码系列文章已经放到了github上,欢迎star: reading ...
- 读Zepto源码之Callbacks模块
Callbacks 模块并不是必备的模块,其作用是管理回调函数,为 Defferred 模块提供支持,Defferred 模块又为 Ajax 模块的 promise 风格提供支持,接下来很快就会分析到 ...
- 读Zepto源码之Deferred模块
Deferred 模块也不是必备的模块,但是 ajax 模块中,要用到 promise 风格,必需引入 Deferred 模块.Deferred 也用到了上一篇文章<读Zepto源码之Callb ...
随机推荐
- selenium 使用随笔
今天闲来无事,有朋友问到,如何使用selenium.于是便便写了个简单的脚本,用于讲解其基础用法. selenium常用作web自动化测试.通过跟python或其他语言的结合,可以驱动浏览器,并在浏览 ...
- How to execute tons of tasks parallelly with TPL method?
List<Task> taskList = new List<Task>(); // string currentNoStr = null; cannot define at ...
- mysql @value := 用法
背景 有这么一张表,记录名字和分数,现在需要按照成绩排名,存在并列名次的情况 解决方法 思路:按照score从大到小排序,第一行数据就是第一名,第二行就是第二名......需要判断当前行的score和 ...
- Vue.js 入门
背景 为了学习spring,准备写一个通讯录demo,实现增删改查功能. 前端页面同事推荐用vue.js,因为简单快速,当然前提是基于你对前端的html,css,js有一定的了解 资料 vue.js ...
- 微信原图泄露的只能是 Exif ,你的隐私不在这!!!
版权声明: 本账号发布文章均来自公众号,承香墨影(cxmyDev),版权归承香墨影所有. 每周会统一更新到这里,如果喜欢,可关注公众号获取最新文章. 未经允许,不得转载. 序 最近很多公众号都推送了关 ...
- shell之算数运算符、逻辑运算符、关系运算符、布尔运算符、文件测试运算符
注意事项: (1)bash不支持简单的数学运算,但是可以通过其他命令来实现,例如 awk 和 expr,expr 最常用: (2)完整的表达式要被 ` ` 包含,注意这个字符不是常用的单引号,在 Es ...
- Tensorflow开发环境配置及其基本概念
Tensorflow开发环境配置及其基本概念 1.1. 安装Tensorflow开发环境 1.1.1. 安装pycharm 1.1.2. 安装pythe3.6 1.1.3. 安装Tensorflow ...
- C++语法细节笔记
1.数据类型转换 当赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数. 把负数转换成无符号数类似于直接给无符号数赋一个负值,结果等于这个负数加上无符号数后的模. ...
- 【Linux部署 · JDK】在linux系统安装jdk
1,检查是否安装jdk echo $JAVA_HOME 或者java -version 2,查看操作系统,很明显这是一个相当old的操作系统,i686是32位操作系统. 3,在oracle ...
- 查询session内容
Enumeration enumsession = request.getSession().getAttributeNames(); while(enumsession.hasMoreElement ...