前言
现在一直在做移动端的开发,这次将单页应用的网页内嵌入了app,于是老大反映了一个问题:
app应用点击响应慢!
我开始不以为然,于是拿着网页版的试了试,好像确实有一定延迟,于是开始了研究,最后选择了touch取代鼠标事件
但是,touch事件取代mouse事件,还是有一定问题的,据说网上问题很多,因为两者之间还是有一定差异
而且如果完全使用touch事件,对自动化测试的同事来说,他们的系统根本不支持touch事件,再者我们平时网页开发也不方便
所以,了解鼠标事件与touch事件的区别,探讨鼠标事件与touch事件的兼容也是有必要的,于是我们开始今天的学习吧
PS:这里使用zepto框架,懒得自己搞了......
事件差异
鼠标事件
首先,我们来看看鼠标事件相关吧:
var startTime;var log = function (msg) { console.log(new Date().getTime() - startTime); console.log(msg);};var mouseDown = function () { startTime = new Date().getTime(); log('mouseDown');};var mouseClick = function () { log('mouseClick');};var mouseUp = function () { log('mouseUp');}; document.addEventListener('mousedown', mouseDown);document.addEventListener('click', mouseClick);document.addEventListener('mouseup', mouseUp);
从这里看到了,鼠标顺序是有mousedown -> click -> mouseup 的顺序,其时间差也出来了
touch事件
然后我们看看touch事件
没有click
touch包含三个事件,touchstart、touchmove、touchend,并没有click事件,所以click事件需要自己模拟,这个我们后面来看看
var startTime;var log = function (msg) { console.log(new Date().getTime() - startTime); console.log(msg);};var touchStart = function () { startTime = new Date().getTime(); log('touchStart');};var touchEnd = function () { log('touchEnd');};document.addEventListener('touchstart', touchStart);document.addEventListener('touchend', touchEnd);
在chrome开启touch事件的情况下,可以看到这个结果
混合事件
现在我们在手机上同时触发两者事件看看区别,这里代码做一定修改
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head> <title></title> <script id="others_zepto_10rc1" type="text/javascript" class="library" src="/images/2021/07/19/03/2021071903074519470002.js"></script></head><body> <div id="d" style="width: 100px; height: 100px; border: 1px solid black;"> </div></body><script type="text/javascript"> var startTime; var log = function (msg) { var div = $('<div></div>'); div.html((new Date().getTime()) + ': ' + (new Date().getTime() - startTime) + ': ' + msg) $('body').append(div); }; var touchStart = function () { startTime = new Date().getTime(); log('touchStart'); }; var touchEnd = function () { log('touchEnd'); }; var mouseDown = function () { log('mouseDown'); }; var mouseClick = function () { log('mouseClick'); }; var mouseUp = function () { log('mouseUp'); }; var d = $('#d'); d.bind('mousedown', mouseDown); d.bind('click', mouseClick); d.bind('mouseup', mouseUp); d.bind('touchstart', touchStart); d.bind('touchend', touchEnd);</script></html>
测试地址
http://sandbox.runjs.cn/show/ey54cgqf
此处手机与电脑有非常大的区别!!!
结论
不要同时给document绑定鼠标与touch事件
document.addEventListener('mousedown', mouseDown);document.addEventListener('click', mouseClick);document.addEventListener('mouseup', mouseUp);document.addEventListener('touchstart', touchStart);document.addEventListener('touchend', touchEnd);
这个样子,在手机上不会触发click事件,click事件要绑定到具体元素
PS:此处的原因我就不去研究了,如果您知道为什么,请留言
手机上mousedown本来响应就慢
经过测试,电脑上touch与click事件的差距不大,但是手机上,当我们手触碰屏幕时,要过300ms左右才会触发mousedown事件
所以click事件在手机上响应就是慢一拍
数据说明
可以看到,在手机上使用click事件其实对用户体验并不好,所以我们可能会逐步使用touch事件
参数差异
现在,我们来看看鼠标与touch事件的参数差异
var startTime;var log = function (msg, e) { console.log(e); var div = $('<div></div>'); div.html((new Date().getTime()) + ': ' + (new Date().getTime() - startTime) + ': ' + msg) $('body').append(div);};var touchStart = function (e) { startTime = new Date().getTime(); log('touchStart', e);};var touchEnd = function (e) { log('touchEnd', e);};var mouseDown = function (e) { log('mouseDown', e);};var mouseClick = function (e) { log('mouseClick', e);};var mouseUp = function (e) { log('mouseUp', e);};var d = $('#d');d.bind('mousedown', mouseDown);d.bind('click', mouseClick);d.bind('mouseup', mouseUp);d.bind('touchstart', touchStart);d.bind('touchend', touchEnd);
事件参数(touchstart/mouseup)
我们来看几个关键的地方:
changedTouches/touches/targetTouches
touches:为屏幕上所有手指的信息
PS:因为手机屏幕支持多点触屏,所以这里的参数就与手机有所不同
targetTouches:手指在目标区域的手指信息
changedTouches:最近一次触发该事件的手指信息
比如两个手指同时触发事件,2个手指都在区域内,则容量为2,如果是先后离开的的话,就会先触发一次再触发一次,这里的length就是1,只统计最新的
PS:一般changedTouches的length都是1
touchend时,touches与targetTouches信息会被删除,changedTouches保存的最后一次的信息,最好用于计算手指信息
这里要使用哪个数据各位自己看着办吧,我也不是十分清晰(我这里还是使用changedTouches吧)
参数信息(changedTouches[0])
几个重要通用点:
① clientX:在显示区的坐标
② pageX:鼠标在页面上的位置
③ screenX:鼠标在显示屏上的坐标(我是双屏所以x很大)
④ target:当前元素
几个重要不同点:
① layerX:这个是相对距离,这个不同,所以不要用这个东西了
② ......
这个有必要说明下,比如我们改下代码:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head> <title></title> <script id="others_zepto_10rc1" type="text/javascript" class="library" src="/images/2021/07/19/03/2021071903074519470002.js"></script> </head><body><div style=" position: relative; width: 500px; height: 300px; border: 1px solid black;"><div id="d" style=" position: absolute; top: 50px; left: 50px; width: 100px; height: 100px; border: 1px solid black;" ></div></div></body><script type="text/javascript">var startTime;var log = function (msg, e) { console.log(e); var div = $('<div></div>'); div.html((new Date().getTime()) + ': ' + (new Date().getTime() - startTime) + ': ' + msg) $('body').append(div);};var touchStart = function (e) { startTime = new Date().getTime(); log('touchStart', e);};var touchEnd = function (e) { log('touchEnd', e);};var mouseDown = function (e) { log('mouseDown', e);};var mouseClick = function (e) { log('mouseClick', e);};var mouseUp = function (e) { log('mouseUp', e);};var d = $('#d');d.bind('mousedown', mouseDown);d.bind('click', mouseClick);d.bind('mouseup', mouseUp);d.bind('touchstart', touchStart);d.bind('touchend', touchEnd); </script></html>
测试地址
http://sandbox.runjs.cn/show/7tyo48bf
各位自己运行看看差异吧
简单扩展touch事件
touch没有click事件,于是有zepto搞了个tap事件,我们这里先来简单模拟一下,再看源码怎么干的
var mouseData = { sTime: 0, eTime: 0, sX: 0, eX: 0, sY: 0, eY: 0};var log = function (msg) { console.log(msg);};var touchStart = function (e) { var pos = e.changedTouches[0]; mouseData.sTime = new Date().getTime(); mouseData.sX = pos.pageX; mouseData.sY = pos.pageY;};var touchMove = function (e) { // var pos = e.changedTouches[0]; // mouseData.eTime = new Date().getTime(); // mouseData.eX = pos.pageX; // mouseData.eY = pos.pageY; e.preventDefault(); return false;};var touchEnd = function (e) { var pos = e.changedTouches[0]; mouseData.eTime = new Date().getTime(); mouseData.eX = pos.pageX; mouseData.eY = pos.pageY; var data = onTouchEnd(); log(data); var d = $('body'); d.append($('<div>间隔:' + data.timeLag + ', 方向:' + data.dir + '</div>'));};var onTouchEnd = function () { //时间间隔 var timeLag = mouseData.eTime - mouseData.sTime; //移动状态,默认乱移动 var dir = 'move'; if (mouseData.sX == mouseData.eX) { if (mouseData.eY - mouseData.sY > 0) dir = 'down'; if (mouseData.eY - mouseData.sY < 0) dir = 'up'; if (mouseData.eY - mouseData.sY == 0) dir = 'tap'; } if (mouseData.sY == mouseData.eY) { if (mouseData.eX - mouseData.sX > 0) dir = 'right'; if (mouseData.eX - mouseData.sX < 0) dir = 'left'; if (mouseData.eX - mouseData.sX == 0) dir = 'tap'; } return { timeLag: timeLag, dir: dir };};var touchEvents = function (el, func) { el = el || document; func = func || function () { }; el.addEventListener('touchstart', touchStart); el.addEventListener('touchmove', touchMove); el.addEventListener('touchend', touchEnd);};var d = $('body');touchEvents(d[0]);
测试地址
http://sandbox.runjs.cn/show/2n9nqssv
这里就可以看到一次touch事件是tap还是up等属性,当然很多时候我们需要设置x方向或者y方向不可拖动,这样就更好呈现
时间间隔长短可以让我们判断自己的拖动是长拖动还是短拖动,长拖动也许用户希望动画慢点,短拖动也许动画就快了
touch事件代码汇总
var log = function (msg) { console.log(msg);};var d = $('body');var touchEvents = function (el, type, func) { this.long = 400; //用于设置长点击阀值 this.el = el || document; this.func = func || function () { }; this.type = type || 'tap'; this.mouseData = { sTime: 0, eTime: 0, sX: 0, eX: 0, sY: 0, eY: 0 }; this.addEvent();};touchEvents.prototype = { constructor: touchEvents, addEvent: function () { var scope = this; this.startFn = function (e) { scope.touchStart.call(scope, e); }; this.moveFn = function (e) { scope.touchMove.call(scope, e); }; this.endFn = function (e) { scope.touchEnd.call(scope, e); }; this.el.addEventListener('touchstart', this.startFn); //此处可以换成这样 // document.addEventListener('touchmove', this.touchMove); this.el.addEventListener('touchmove', this.moveFn); this.el.addEventListener('touchend', this.endFn); }, removeEvent: function () { this.el.removeEventListener('touchstart', this.touchStart); this.el.removeEventListener('touchmove', this.touchMove); this.el.removeEventListener('touchend', this.touchEnd); }, touchStart: function (e) { var pos = e.changedTouches[0]; this.mouseData.sTime = new Date().getTime(); this.mouseData.sX = pos.pageX; this.mouseData.sY = pos.pageY; }, touchMove: function (e) { e.preventDefault(); return false; }, touchEnd: function (e) { var pos = e.changedTouches[0]; this.mouseData.eTime = new Date().getTime(); this.mouseData.eX = pos.pageX; this.mouseData.eY = pos.pageY; this.onTouchEnd(); }, onTouchEnd: function () { if (this.type == this._getDir()) { } }, _getDir: function () { //时间间隔,间隔小于100都认为是快速,大于400的认为是慢速 var timeLag = this.mouseData.eTime - this.mouseData.sTime; var dir = 'swipe'; if (timeLag > this.long) dir = 'longSwipe'; if (this.mouseData.sX == this.mouseData.eX && this.mouseData.sY == this.mouseData.eY) { dir = 'tap'; if (timeLag > this.long) dir = 'longTap'; } else { if (Math.abs(this.mouseData.eY - this.mouseData.sY) > Math.abs(this.mouseData.eX - this.mouseData.sX)) { dir = this._getUDDir(dir); } else { dir = 'swipe'; dir = this._getLRDir(dir); } } log(dir); d.append($('<div>间隔:' + timeLag + ', 方向:' + dir + '</div>')); return dir; }, //单独用于计算上下的 _getUDDir: function (dir) { if (this.mouseData.eY - this.mouseData.sY > 0) dir += 'Down'; if (this.mouseData.eY - this.mouseData.sY < 0) dir += 'Up'; return dir; }, //计算左右 _getLRDir: function (dir) { if (this.mouseData.eX - this.mouseData.sX > 0) dir += 'Right'; if (this.mouseData.eX - this.mouseData.sX < 0) dir += 'Left'; return dir; }};new touchEvents(d[0], 'swipe', function () {// d.append($('<div>间隔:' + data.timeLag + ', 方向:' + data.dir + '</div>'));});
测试地址
http://sandbox.runjs.cn/show/rpohk79w
测试时请使用chrome,并且开启touch事件
测试效果
完整可绑定事件代码
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head> <title></title> <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"> <script id="others_zepto_10rc1" type="text/javascript" class="library" src="/images/2021/07/19/03/2021071903074519470002.js"></script></head><body> <div id="d" style="position: absolute; top: 50px; left: 50px; width: 100px; height: 100px; border: 1px solid black;">滑动我 </div></body><script type="text/javascript">var log = function (msg) { console.log(msg);};var d = $('body');var touchEvents = function (el, type, func) { this.long = 400; //用于设置长点击阀值 this.el = el || document; this.func = func || function () { }; this.type = type || 'tap'; this.mouseData = { sTime: 0, eTime: 0, sX: 0, eX: 0, sY: 0, eY: 0 }; this.addEvent();};touchEvents.prototype = { constructor: touchEvents, addEvent: function () { var scope = this; this.startFn = function (e) { scope.touchStart.call(scope, e); }; this.moveFn = function (e) { scope.touchMove.call(scope, e); }; this.endFn = function (e) { scope.touchEnd.call(scope, e); }; this.el.addEventListener('touchstart', this.startFn); //此处可以换成这样 // document.addEventListener('touchmove', this.touchMove); this.el.addEventListener('touchmove', this.moveFn); this.el.addEventListener('touchend', this.endFn); }, removeEvent: function () { this.el.removeEventListener('touchstart', this.touchStart); this.el.removeEventListener('touchmove', this.touchMove); this.el.removeEventListener('touchend', this.touchEnd); }, touchStart: function (e) { var pos = e.changedTouches[0]; this.mouseData.sTime = new Date().getTime(); this.mouseData.sX = pos.pageX; this.mouseData.sY = pos.pageY; }, touchMove: function (e) { e.preventDefault(); return false; }, touchEnd: function (e) { var pos = e.changedTouches[0]; this.mouseData.eTime = new Date().getTime(); this.mouseData.eX = pos.pageX; this.mouseData.eY = pos.pageY; this.onTouchEnd(e); }, onTouchEnd: function (e) { if (this.type == this._getDir()) { this.func(e, this); } }, _getDir: function () { //时间间隔,间隔小于100都认为是快速,大于400的认为是慢速 var timeLag = this.mouseData.eTime - this.mouseData.sTime; var dir = 'swipe'; if (timeLag > this.long) dir = 'longSwipe'; if (this.mouseData.sX == this.mouseData.eX && this.mouseData.sY == this.mouseData.eY) { dir = 'tap'; if (timeLag > this.long) dir = 'longTap'; } else { if (Math.abs(this.mouseData.eY - this.mouseData.sY) > Math.abs(this.mouseData.eX - this.mouseData.sX)) { dir = this._getUDDir(dir); } else { dir = this._getLRDir(dir); } } log(dir); d.append($('<div>间隔:' + timeLag + ', 方向:' + dir + '</div>')); return dir; }, //单独用于计算上下的 _getUDDir: function (dir) { if (this.mouseData.eY - this.mouseData.sY > 0) dir += 'Down'; if (this.mouseData.eY - this.mouseData.sY < 0) dir += 'Up'; return dir; }, //计算左右 _getLRDir: function (dir) { if (this.mouseData.eX - this.mouseData.sX > 0) dir += 'Right'; if (this.mouseData.eX - this.mouseData.sX < 0) dir += 'Left'; return dir; }};new touchEvents(d[0], 'tap', function (e) { log(arguments);});</script></html>
这个代码基本可用了,但是使用上不是很方便,我们这里就不关注了,下面我们来看看zepto的代码和兼容问题
zepto的touch与兼容
先上zepto源码,一看就知道我写的有多不行啦!
(function ($) { var touch = {}, touchTimeout, tapTimeout, swipeTimeout, longTapDelay = 750, longTapTimeout function parentIfText(node) { return 'tagName' in node ? node : node.parentNode } function swipeDirection(x1, x2, y1, y2) { var xDelta = Math.abs(x1 - x2), yDelta = Math.abs(y1 - y2) return xDelta >= yDelta ? (x1 - x2 > 0 ? 'Left' : 'Right') : (y1 - y2 > 0 ? 'Up' : 'Down') } function longTap() { longTapTimeout = null if (touch.last) { touch.el.trigger('longTap') touch = {} } } 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 = {} } $(document).ready(function () { var now, delta $(document.body) .bind('touchstart', function (e) { now = Date.now() delta = now - (touch.last || now) touch.el = $(parentIfText(e.touches[0].target)) touchTimeout && clearTimeout(touchTimeout) touch.x1 = e.touches[0].pageX touch.y1 = e.touches[0].pageY if (delta > 0 && delta <= 250) touch.isDoubleTap = true touch.last = now longTapTimeout = setTimeout(longTap, longTapDelay) }) .bind('touchmove', function (e) { cancelLongTap() touch.x2 = e.touches[0].pageX touch.y2 = e.touches[0].pageY if (Math.abs(touch.x1 - touch.x2) > 10) e.preventDefault() }) .bind('touchend', function (e) { 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 else if ('last' in touch) // delay by one tick so we can cancel the 'tap' event if 'scroll' fires // ('tap' fires before 'scroll') 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) { touch.el.trigger('doubleTap') touch = {} } // trigger single tap after 250ms of inactivity else { touchTimeout = setTimeout(function () { touchTimeout = null touch.el.trigger('singleTap') touch = {} }, 250) } }, 0) }) .bind('touchcancel', cancelAll) $(window).bind('scroll', cancelAll) }) ; ['swipe', 'swipeLeft', 'swipeRight', 'swipeUp', 'swipeDown', 'doubleTap', 'tap', 'singleTap', 'longTap'].forEach(function (m) { $.fn[m] = function (callback) { return this.bind(m, callback) } })})(Zepto)
touch对象与上面mouseData功效相同,记录一些属性
delta 用于记录两次点击的间隔,间隔短就是双击
swipeDirection 函数与_getDir _getUDDir _getLRDir 功能相似,只不过代码更为简练,并且真正的私有化了
63行代码开始,若是代码移动过便是划屏,否则就是点击,这点我也没考虑到
73行,否则就应该是点击,这里并且判断是否存在结束时间,代码比较健壮,做了双击或者快速点击的判断
开始兼容
zepto代码我自然没有资格去评说,现在我们来看看他的兼容问题
PS:我这里很水,不太敢动源码,就加一个tap判断,因为也只是用了这个,具体大动手脚的事情,我们后面再做
这样做事因为,我们的项目主要是把click改成了tap事件,导致页面很多功能不可用
['swipe', 'swipeLeft', 'swipeRight', 'swipeUp', 'swipeDown', 'doubleTap', 'tap', 'singleTap', 'longTap'].forEach(function (m) { //兼容性方案处理,以及后期资源清理,如果为假时候,就触发点击事件 var isTouch = 'ontouchstart' in document.documentElement; if(m === 'tap' && isTouch === false) { $.fn[m] = function (callback) { return this.bind('click', callback) } } else { $.fn[m] = function (callback) { return this.bind(m, callback) } } })
我就干了这么一点点事情......
感谢作者 叶小钗 给我们带来经精彩的文章!