FastClick 填坑及原始碼解析

發表於2016-05-25

最近產品妹子提出了一個體驗issue —— 用 iOS 在手Q閱讀書友交流區發表書評時,游標點選總是不好定位到正確的位置:

如上圖,具體表現是較快點選時,游標總會跳到 textarea 內容的尾部。只有當點選停留時間較久一點(比如超過150ms)才能把游標正常定位到正確的位置。

一開始我以為是 iOS 原生的互動問題沒太在意,但後來發現訪問某些頁面又是沒有這種奇怪體驗的。

然後懷疑是否 JS 註冊了某些事件導致的問題,於是試著把業務模組移除了再跑一遍,發現問題照舊。

於是只好繼續做排除法,把頁面上的一些庫一點點移掉再執行頁面,結果發現搗亂的小鬼果然是嫌疑最大的 Fastclick。

然後呢,我試著按API所說,給 textarea 加上一個名為“needsclick”的類名,希望能繞過 fastclick 的處理直接走原生點選事件,結果訝異地發現屁用沒有。。。

對此感謝後面我們小組的 kindeng 童鞋幫忙研究了下並提供瞭解決方案,不過我還想進一步研究到底是什麼原因導致了這個坑、Fastclick 對我的頁面做了神馬~

所以昨晚花了點時間一口氣把原始碼都蹂躪了一遍。

這會是一篇很長的文章,但會是註釋非常詳盡的剖析文。

文章帶分析的原始碼我也掛在我的 github 倉庫上了,有興趣的童鞋可以去下載來看。

閒話不多說,我們們開始深入 FastClick 原始碼陣營。

我們知道,註冊一個 FastClick 事件非常簡單,它是這樣的:

所以我們從這裡著手,開啟原始碼看下 FastClick .attach 方法:

這裡返回了一個 FastClick 例項,所以我們們拉到前面看看 FastClick 建構函式:

在初始通過 FastClick.notNeeded 方法判斷是否需要做後續的相關處理:

我們看下這個 FastClick.notNeeded 都做了哪些判斷:

基本上都是一些能禁用 300ms 時延的瀏覽器嗅探,它們都沒必要使用 Fastclick,所以會返回 true 回建構函式停止下一步執行。

由於安卓手Q的 ua 會被匹配到 /Chrome/([0-9]+)/,故帶有 ‘user-scalable=no’ meta 標籤的安卓手Q頁會被 FastClick 視為無需處理頁。

這也是為何在安卓手Q裡沒有開頭提及問題的原因。

我們繼續看建構函式,它直接給 layer(即body)新增了click、touchstart、touchmove、touchend、touchcancel(若是安卓還有 mouseover、mousedown、mouseup)事件監聽:

注意在這段程式碼上面還利用了 bind 方法做了處理,這些事件回撥中的 this 都會變成 Fastclick 例項上下文。

另外還得留意,onclick 事件以及安卓的額外處理部分都是走的捕獲監聽。

我們們分別看看這些事件回撥分別都做了什麼。

1. this.onTouchStart

順道看下這裡的 this.updateScrollParent:

另外要注意的是,在 onTouchStart 裡被標記為 true 的 this.trackingClick 屬性,都會在其它事件回撥(比如 ontouchmove )的開頭做檢測,如果沒被賦值過,則直接忽略:

當然在 ontouchend 事件裡會把它重置為 false。

2. this.onTouchMove

這段程式碼量好少:

看下這裡用到的 this.touchHasMoved 原型方法:

3. onTouchEnd

這段比較長,我們主要看這段:

其中 this.needsFocus 用於判斷給定元素是否需要通過合成click事件來模擬聚焦:

另外這段說明了為何稍微久按一點(超過100ms)textarea ,我們是可以把游標定位在正確的地方(會繞過後面呼叫 this.focus 的方法)

接著我們們看看這兩行很重要的程式碼:

所涉及的兩個原型方法分別為:

⑴ this.focus

注意,我們點選 textarea 時呼叫了該方法,它通過 targetElement.setSelectionRange(length, length) 決定了游標的位置在內容的尾部(但注意,這時候還沒聚焦!!!)。

⑵ this.sendClick

真正讓 textarea 聚焦的是這個方法,它合成了一個 click 方法立刻在textarea元素上觸發導致聚焦:

經過這麼一折騰,我們們輕點 textarea 後,游標就自然定位到其內容尾部去了。但是這裡有個問題——排在 touchend 後的 focus 事件為啥沒被觸發呢?

如果 focus 事件能被觸發的話,那肯定能重新定位游標到正確的位置呀。

我們們看下面這段:

通過 preventDefault 的阻擋,textarea 自然再也無法擁抱其 focus 寶寶了~

於是乎,我們在這裡做個改動就能修復這個問題:

或者:

這裡要吐槽下的是,Fastclick 把 this.needsClick 放到了 ontouchEnd 末尾去執行,才導致前面說的加上了“needsclick”類名也無效的問題。

雖然問題原因找到也解決了,但我們們還是繼續看剩下的部分吧。

4. onMouse 和 onClick

常規需要阻斷點選事件的操作,我們在 touch 監聽事件回撥中已經做了處理,這裡主要是針對那些 touch 過程(有些裝置甚至可能並沒有touch事件觸發)沒有禁用預設事件的 event 做進一步處理,從而決定是否觸發原生的 click 事件(如果禁止是在 onMouse 方法裡做的處理)。

小結

1. 在 fastclick 原始碼的 addEventListener 回撥事件中有很多的 return false/true。它們其實主要用於繞過後面的指令碼邏輯,並沒有其它意義(它是不會阻止預設事件的)。

所以千萬別把 jQuery 事件、或者 DOM0 級事件回撥中的 return false 概念,跟 addEventListener 的混在一起了。

2. fastclick 的原始碼其實很簡單,有很大部分不外乎對一些怪異行為做 hack,其核心理念不外乎是——捕獲 target 事件,判斷 target 是要解決點透問題的元素,就合成一個 click 事件在 target 上觸發,同時通過 preventDefault 禁用預設事件。

3. fastclick 雖好,但也有一些坑,還是得按需求對其修改,那麼瞭解其原始碼還是很有必要的。

相關文章