移动端click事件延迟300ms的原因以及解决办法
这要追溯至 2007 年初。苹果公司在发布首款 iPhone 前夕,遇到一个问题 —— 当时的网站都是为大屏幕设备所设计的。于是苹果的工程师们做了一些约定,应对 iPhone 这种小屏幕浏览桌面端站点的问题。这当中最出名的,当属双击缩放(double tap to zoom)。这也是会有上述 300 毫秒延迟的主要原因。
当用户一次点击屏幕之后,浏览器并不能立刻判断用户是要进行双击缩放,还是想要进行单击操作。因此,iOS Safari 就等待 300 毫秒,以判断用户是否再次点击了屏幕。
于是,300 毫秒延迟就这么诞生了。
FastClick 是 FT Labs 专门为解决移动端浏览器 300 毫秒点击延迟问题所开发的一个轻量级的库。简而言之,FastClick 在检测到 touchend
事件的时候,会通过 DOM 自定义事件立即触发一个模拟click
事件的click事件(自定义事件),并把浏览器在 300 毫秒之后真正触发的 click
事件阻止掉。
FastClick 非常实际地解决 300 毫秒点击延迟的问题。唯一的缺点可能也就是该脚本的文件尺寸 (尽管它不大)。如果你非常在意这点文件大小,可以尝试一下 Filament Group 的 Tappy!,或者 tap.js。两者都相当轻量,能够通过监听 tap
而非 click
事件来绕过 300 毫秒延迟。
当然,zepto库函数中,也有一个touch模块,此模块也包含tap事件,可以通过绑定tap来取代click事件。
但是zepto的tap事件会有点透问题。如何解决,请看下篇分解。
接下来,我们来详细了解一个问题:FastClick解决延迟点击的源码解析。
FastClick解决延迟点击的源码解析。
首先,我们来看FastClick的使用。
1 2 3 | window.addEventListener( "load" , function () { FastClick.attach( document.body ); }, false ); |
这样就解决了 300 毫秒点击延迟的问题。
FastClick的源码:
1 2 3 4 | FastClick.attach = function (layer) { 'use strict' ; return new FastClick(layer); }; |
在FastClick的构造函数中,会有下面这段代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | this .onClick = function () { return FastClick.prototype.onClick.apply(self, arguments); }; this .onMouse = function () { return FastClick.prototype.onMouse.apply(self, arguments); }; this .onTouchStart = function () { return FastClick.prototype.onTouchStart.apply(self, arguments); }; this .onTouchEnd = function () { return FastClick.prototype.onTouchEnd.apply(self, arguments); }; this .onTouchCancel = function () { return FastClick.prototype.onTouchCancel.apply(self, arguments); }; if (FastClick.notNeeded(layer)) { return ; } if ( this .deviceIsAndroid) { layer.addEventListener( 'mouseover' , this .onMouse, true ); layer.addEventListener( 'mousedown' , this .onMouse, true ); layer.addEventListener( 'mouseup' , this .onMouse, true ); } layer.addEventListener( 'click' , this .onClick, true ); layer.addEventListener( 'touchstart' , this .onTouchStart, false ); layer.addEventListener( 'touchend' , this .onTouchEnd, false ); layer.addEventListener( 'touchcancel' , this .onTouchCancel, false ); |
也就是在document.body上绑定了click,touchstart,touchend,touchcancel事件。
这里假设,我们的页面有一个button,绑定了click事件。
当用户点击此button时,会先触发touchstart事件,这时,会冒泡到document.body中,于是就会执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | FastClick.prototype.onTouchStart = function (event) { 'use strict' ; var targetElement, touch, selection; if (event.targetTouches.length > 1) { return true ; } targetElement = this .getTargetElementFromEventTarget(event.target); touch = event.targetTouches[0]; if ( this .deviceIsIOS) { selection = window.getSelection(); if (selection.rangeCount && !selection.isCollapsed) { return true ; } if (! this .deviceIsIOS4) { if (touch.identifier === this .lastTouchIdentifier) { event.preventDefault(); return false ; } this .lastTouchIdentifier = touch.identifier; this .updateScrollParent(targetElement); } } this .trackingClick = true ; this .trackingClickStart = event.timeStamp; this .targetElement = targetElement; this .touchStartX = touch.pageX; this .touchStartY = touch.pageY; if ((event.timeStamp - this .lastClickTime) < 200) { event.preventDefault(); } return true ; }; |
这个回调函数主要做了以下事情:
获取我们当前触发touchstart的元素,这里是button。
然后将鼠标的信息记录了下来,记录鼠标的信息主要是为了在后面touchend触发时,根据这里得到的x、y判断是否为click。
之后,会触发touchend事件,然后冒泡到document.body上,执行以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | FastClick.prototype.onTouchEnd = function (event) { 'use strict' ; var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this .targetElement; if ( this .touchHasMoved(event) || (event.timeStamp - this .trackingClickStart) > 300) { this .trackingClick = false ; this .targetElement = null ; } if (! this .trackingClick) { return true ; } if ((event.timeStamp - this .lastClickTime) < 200) { this .cancelNextClick = true ; return true ; } this .lastClickTime = event.timeStamp; trackingClickStart = this .trackingClickStart; this .trackingClick = false ; this .trackingClickStart = 0; if ( this .deviceIsIOSWithBadTarget) { touch = event.changedTouches[0]; targetElement = document.elementFromPoint(touch.pageX - window.pageXOffset, touch.pageY - window.pageYOffset); } targetTagName = targetElement.tagName.toLowerCase(); if (targetTagName === 'label' ) { forElement = this .findControl(targetElement); if (forElement) { this .focus(targetElement); if ( this .deviceIsAndroid) { return false ; } targetElement = forElement; } } else if ( this .needsFocus(targetElement)) { if ((event.timeStamp - trackingClickStart) > 100 || ( this .deviceIsIOS && window.top !== window && targetTagName === 'input' )) { this .targetElement = null ; return false ; } this .focus(targetElement); if (! this .deviceIsIOS4 || targetTagName !== 'select' ) { this .targetElement = null ; event.preventDefault(); } return false ; } if ( this .deviceIsIOS && ! this .deviceIsIOS4) { scrollParent = targetElement.fastClickScrollParent; if (scrollParent && scrollParent.fastClickLastScrollTop !== scrollParent.scrollTop) { return true ; } } if (! this .needsClick(targetElement)) { event.preventDefault(); this .sendClick(targetElement, event); } return false ; }; |
注意上面的代码中,event.preventDefault();会阻止真实的click事件的触发,因此,在button上面的click事件不会触发。
接下来,我们只需要查看sendClick方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 | FastClick.prototype.sendClick = function (targetElement, event) { 'use strict' ; var clickEvent, touch; if (document.activeElement && document.activeElement !== targetElement) { document.activeElement.blur(); } touch = event.changedTouches[0]; clickEvent = document.createEvent( 'MouseEvents' );<br> clickEvent.initMouseEvent( 'click' , true , true , window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false , false , false , false , 0, null ); clickEvent.forwardedTouchEvent = true ; targetElement.dispatchEvent(clickEvent); }; |
在此方法中,会创建一个自定义的click事件,然后在button上立即触发,于是,button绑定的click的事件回调函数马上执行,因此就没有300ms延迟了。
上面的initMouseEvent方法的前三个参数的意思:1.事件类型,2.是否冒泡,3.是否阻止浏览器的默认行为。
自定义的click事件阻止了浏览器的默认行为,事件冒泡,于是执行document.body的click事件回调函数。代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | FastClick.prototype.onClick = function (event) { 'use strict' ; var permitted; if ( this .trackingClick) { this .targetElement = null ; this .trackingClick = false ; return true ; } if (event.target.type === 'submit' && event.detail === 0) { return true ; } permitted = this .onMouse(event); if (!permitted) { this .targetElement = null ; } return permitted; }; |
然后里面有一句 permitted = this.onMouse(event);于是,我们查看onMouse方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | FastClick.prototype.onMouse = function (event) { 'use strict' ; if (! this .targetElement) { return true ; } if (event.forwardedTouchEvent) { return true ; } if (!event.cancelable) { return true ; } if (! this .needsClick( this .targetElement) || this .cancelNextClick) { if (event.stopImmediatePropagation) { event.stopImmediatePropagation(); } else { event.propagationStopped = true ; } event.stopPropagation(); event.preventDefault(); return false ; } return true ; }; |
此方法,会阻止模拟的click事件的冒泡以及默认行为。
到此,解决了移动端浏览器click事件延迟300ms的问题。
加油!
posted on 2015-05-22 17:31 chaojidan 阅读(19508) 评论(0) 编辑 收藏 举报
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?