最近發現升級到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);