最近发现升级到ios11.3以后,输入框点击变得不灵敏,第二次点击页面中的输入框须要长按一会才能正常唤起键盘输入。排查后,怀疑是fastclick出现了问题,上github看了issues,果不其然不少人也出现相同问题(https://github.com/ftlabs/fas... )。按照issues上的解决方法,也顺利地解决了问题,不过,究竟为什么会出现这么奇怪的bug?咱们还须要继续深刻寻找答案。html
简而言之,它是用来解决300ms延迟和点击穿透这两个问题。
在移动设备上点击按钮后,浏览器将会等待300ms,继续监听点击动做来判断是否为双击事件,这就是300ms延迟问题。
为了解决这300ms的延迟问题,一种解决方案是将touch系列事件绑定在document上,经过计算touch事件触发的时间位置等来判断是否为移动设备的点击,如zepto.js中自定义的tap事件;另外一种方案,也是fastclick中的实现方案,当检测到touchend事件的时候,会经过DOM自定义事件当即出发模拟一个click事件,并用preventDefault阻止300ms以后真正的click事件。ios
那么什么是点击穿透问题?
点击穿透问题是当两个元素重叠在同一个位置,上层元素绑定touch事件,下层元素绑定click事件,当上层元素触发touch事件后,可能会触发下层div的click事件。git
fastclick的主要工做可见参考文献[2]中的图,以下:github
fastclick的主要工做是在body或者顶层元素中绑定touch相关事件,在touch相关事件中标记手势的位置与时间,根据此信息拦截click事件并判断是否模拟触发。浏览器
在处理300ms延迟的过程当中,主要工做是模拟并拦截真正的click事件。
首先,拦截点击事件的思路是将元素的onclick事件置为空,并用addEventListener从新绑定,理由是onclick将会在fastclick模拟的点击事件以前触发,在构造函数中关键代码以下:app
function FastClick(layer, options) { ... // If a handler is already declared in the element's onclick attribute, it will be fired before // FastClick's onClick handler. Fix this by pulling out the user-defined handler function and // adding it as listener. if (typeof layer.onclick === 'function') { oldOnClick = layer.onclick; layer.addEventListener('click', function(event) { oldOnClick(event); }, false); layer.onclick = null; } }
接着,看看fastclick如何判断用户的点击事件是真正的点击,在onTouchEnd事件中,判断的关键代码以下:函数
// event.timeStamp为touchend事件的事件,lastClickTime是上一次touchend事件的事件,此处判断是否为双击操做 if ((event.timeStamp - this.lastClickTime) < this.tapDelay) { this.cancelNextClick = true; return true; } // trackingClickStart是touchstart事件的事件,此处判断是否为长按操做 if ((event.timeStamp - this.trackingClickStart) > this.tapTimeout) { return true; }
若是这次点击是真正的点击事件,有两种状况要触发模拟的click事件:一种是由needsFocus函数判断是否为能够focus的元素,如<input type="text">、textarea等;另外一种是由needsClick函数判断是否为须要原生点击的原生,不须要原生点击的也须要模拟click事件,这部分的代码逻辑比较简单主要根据判断元素的tagName和class来判断,这里就不贴代码了。性能
须要触发模拟click事件的状况中,第一种状况(如输入框等)是须要触发focus事件的,触发以后再触发click事件,而第二种(如按钮等)则单纯触发click事件便可。接下来,咱们先分析focus事件的响应函数,再看模拟的click事件。
focus主要工做一方面在为了将光标移到移到输入框尾部,另外一方面触发元素的focus事件,其响应函数为:this
FastClick.prototype.focus = function(targetElement) { var length; if (deviceIsIOS && targetElement.setSelectionRange && targetElement.type.indexOf('date') !== 0 && targetElement.type !== 'time' && targetElement.type !== 'month' && targetElement.type !== 'email') { // 经过 targetElement.setSelectionRange(length, length) 将光标的位置定位在内容的尾部(但注意,这时候还没触发focus事件) length = targetElement.value.length; targetElement.setSelectionRange(length, length); } else { targetElement.focus(); } };
模拟的click事件,本质就是用代码建立一个Event做为点击事件触发,关键代码以下:spa
FastClick.prototype.sendClick = function(targetElement, event) { ... // Synthesise a click event, with an extra attribute so it can be tracked clickEvent = document.createEvent('MouseEvents'); clickEvent.initMouseEvent(this.determineEventType(targetElement), true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null); clickEvent.forwardedTouchEvent = true; targetElement.dispatchEvent(clickEvent); };
最后,fastclick使用了preventDefault和stopImmediatePropagation拦截原生的click响应函数。preventDefault函数很常见了,但stopImmediatePropagation真是头一次见它。根据规范可知,该方法不只能够阻止冒泡,还能将元素绑定的后序相同类型事件的监听函数的执行也一块儿阻止了,也就是说若是在点击事件中调用了它,能够阻止点击事件冒泡传递到父级元素,同时又能阻止该元素上的其余点击响应函数。
Issues中给出的修复方法是强制元素focus,即在改写的focus响应函数中直接触发元素的focus事件:
FastClick.prototype.focus = function(targetElement) { targetElement.focus(); };
推测缘由是因为ios11.3取消了input元素setSelectionRange自动聚焦的功能(非此缘由 (⊙﹏⊙))
(6.22更新) 对比了一下ios11.3与以前的fastclick相关运行过程,只有320行左右有区别,“document.activeElement.blur();”,ios11.3以后在第二次点击时有通过,而ios11.3以前的没有。另外,350行左右的“targetElement.setSelectionRange(length, length);”,是引发输入框聚焦的缘由,但仅仅执行这个函数还没法到达聚焦的效果,fastclick还作了哪些相关工做,仍未知。
此外,ios11.3支持了Web API:容许对事件支持被动模式,减小滚动屏幕的性能损耗和奔溃,而且针对document的touch事件监听添加被动模式的配置,所以document将再也不调用preventDefault方法。这些改动会引发fastclick的另外一个bug,当静置app或锁屏几秒后页面将没法响应任何点击操做。
解决方法也很简单,只需去除被动模式,以下:
// 支持设置passive的,将被动模式显式设置为false layer.addEventListener('touchstart', this.onTouchStart, {passive:false}); // 不然,去除默认的被动模式 layer.addEventListener('touchstart', this.onTouchStart, false);