2019 再聊移動端 300ms 延遲及 fastClick 原理解析

前言

最近公司新開了一條業務線,有幸和大佬們一起從頭開始構建一套適合新業務的框架。俗話說得好呀,適合自己的纔是最好的 😎。在新項目的 CodeReview 的時候,被大哥提到有沒有添加 fastClick 解決移動端 300ms 延遲的問題。以下就帶你追溯移動端延遲的 前世 今生

介紹

前世 - 誕生的因

國外有一篇關於 300ms 延遲的文章:What Exactly Is..... The 300ms Click Delay

世間萬物皆有因果,網頁興起於桌面端,那時候有誰會想到手機等移動設備的風靡?猶記得上大學那會兒,手機訪問學校網站的時候都是通過手指縮放來控制的 🙃,心裏真的是一萬頭草泥馬奔騰而過,後來爲了解決移動端適配的問題,提出了 viewport 的解決方案,基於 無障礙(accessibility)(需要代理)交互設計師爲了更好的用戶體驗,特地提供了 雙擊縮放 的手勢支持。殊不知這正是一切禍亂的根源。

今生 - 消逝的果

谷歌有開發者文檔: 300ms tap delay, gone away(需要代理)

以下是原文的部分引用

For many years, mobile browsers applied a 300-350ms delay between touchend and click while they waited to see if this was going to be a double-tap or not, since double-tap was a gesture to zoom into text.

大致是說,移動瀏覽器 會在 touchendclick 事件之間,等待 300 - 350 ms,判斷用戶是否會進行雙擊手勢用以縮放文字。

Ever since the first release of Chrome for Android, this delay was removed if pinch-zoom was also disabled. However, pinch zoom is an important accessibility feature. As of Chrome 32 (back in 2014) this delay is gone for mobile-optimized sites, without removing pinch-zooming! Firefox and IE/Edge did the same shortly afterwards, and in March 2016 a similar fix landed in iOS 9.3.

從上面我們可以獲取到幾個非常重要的信息:首先,谷歌就開始吹啦,自打我們移動版 Chrome 發佈以來,只要你把縮放禁用掉,這個延遲就不會出現。不得不吹一波 Google,真的是甩開 Apple 幾條街,fastClick 源碼大部分都是用來解決 iOS 各個版本各種奇奇怪怪的 BUG。說實話,有些源碼我也不是很理解,但是咱啥也不敢說,啥也不敢問啊 😂。其次,Chrome 32 對移動端進行了優化,可以不禁用縮放,也能解決延遲的問題。接着 Firefox 和 IE/Edge 緊隨其後也修復了這個 BUG,最後,就是 iOS 9.3 也同樣修復這個 BUG (親測的確修復了)。

解決方案

以下可以通過 hack 技巧,不添加 fastClick 也能修復延遲的問題

禁用縮放

  • Chrome on Android (all versions)
  • iOS 9.3
<meta name="viewport" content="user-scalable=no" />

或者

html {
  touch-action: manipulation;
}
  • IE on Windows Phone
html {
  touch-action: manipulation; // IE11+
  -ms-touch-action: manipulation; // IE10
}

不禁用縮放

  • Chrome 32+ on Android
  • iOS 9.3
<meta name="viewport" content="width=device-width" />

經測試,如果不添加 width=device-width 不管是 Android 還是 iOS 在已修復的版本中仍然會出現延時的問題。

WebView

上面說了這麼多,都是針對移動端瀏覽器的,既然是提到移動端,WebView 當然不得不說啦。

Android WebView

如何設計一個優雅健壯的 Android WebView?

Android WebView 中 300ms 的延遲問題和移動端瀏覽器解決思路一致。

iOS WebView

UIWebView

In apps that run in iOS 8 and later, use the WKWebView class instead of using UIWebView. Additionally, consider setting the WKPreferences property javaScriptEnabled to false if you render files that are not supposed to run JavaScript.

iOS WebView 就有點讓人頭疼了。因爲 iOS 8 之前一直都是 UIWebView,iOS 8 出了個新秀 WKWebView,那麼 iOS 9.3 300ms 延遲的 BUG 修復到底幹了啥呢?在客戶端 iOS 小姐姐的幫助下,最終的測試結果是 UIWebView 300ms 延遲的問題到現在一直存在,哪怕是最新的 iOS 版本(這大概這就是爲什麼老外推薦使用 WKWebView 而非 UIWebView,估計是不想修 BUG 了吧 😂),但是 WKWebView 在 iOS 9.3 的時候將這個問題給修復了。也就是說 iOS 9.3 之前 WKWebView 仍然是存在 300ms 延遲的問題的(忙活了半天,總算把所有的都給理清楚了 🙄)。

FastClick 原理解析

這部分可能有點爛大街了,網上一搜一大把,再說也沒啥意思,我就挑點個人覺得有意思的說一下 😁。

原理

首先,講一下 fastClick 的實現原理吧,MDN 上 同時支持觸屏事件和鼠標事件 也有提到。

移動端,當用戶點擊屏幕時,會依次觸發 touchstarttouchmove(0 次或多次),touchendmousemovemousedownmouseupclicktouchmove 。只有當手指在屏幕發生移動的時候纔會觸發 touchmove 事件。在 touchstarttouchmove 或者 touchend 事件中的任意一個調用 event.preventDefaultmouse 事件 以及 click 事件將不會觸發。

fastClick 在 touchend 階段 調用 event.preventDefault,然後通過 document.createEvent 創建一個 MouseEvents,然後 通過 event​Target​.dispatch​Event 觸發對應目標元素上綁定的 click 事件。

你不知道的 JavaScript (Maybe)

首先,我們需要明確一個問題,300ms 的延遲只有在移動端纔會出現,PC 端是沒有的。fastClick 中又有個一 notNeeded 的函數是用來判斷有沒有必要使用 fastClick。剛開始的時候,剛開始我閱讀完代碼表示對沒有進行移動端和 PC 端的區分表示不滿。不過後來一段不起眼的代碼改變了我的看法。

// Devices that don't support touch don't need FastClick
if (typeof window.ontouchstart === 'undefined') {
  return true;
}

PC 端是沒有 touch 事件的因此 window.ontouchstart 返回 undefined,移動端如果沒有綁定事件則返回 null。果然只能證明我還是太過年輕 🤣。

閱讀源碼期間,無意中發現使用事件委託時,Safari 手機版會有一個 bug,當點擊事件不是綁定在交互式的元素上(比如說 HTML 的 div),並且也沒有直接的事件監聽器綁定在他們自身。不會觸發 click 事件。具體可以參考 click 瀏覽器兼容性

解決方法如下:(請原諒我厚顏無恥地直接搬過來了 😝)

  • 爲其元素或者祖先元素,添加 cursor: pointer 的樣式,使元素具有交互式點擊
  • 爲需要交互式點擊的元素添加 onclick="void(0)" 的屬性,但並不包括 body 元素
  • 使用可點擊元素如 <a>,代替不可交互式元素如 div
  • 不使用 click 的事件委託。

event.stopPropagation 只會阻止相同類型(event.type 相同)事件傳播,上面有提到過 移動端 觸摸事件觸發的順序問題,假如 我在 touchstart 中調用了 event.stopPropagation 只會 阻止後續 event flow 上其他 touchstart 事件,並不會阻止 touchmovetouchend 等 mouseEvent 事件的發生。

event.stopPropagationevent​.stop​Immediate​Propagation的區別你真的知道嗎 🧐,event.stopPropagation 阻止捕獲和冒泡階段中當前事件的進一步傳播。如果有多個相同類型事件的事件監聽函數綁定到同一個元素,當該類型的事件觸發時,它們會按照被添加的順序執行。如果其中某個監聽函數執行 event.stopImmediatePropagation 方法,則當前元素剩下的監聽函數將不會被執行。

event​Target​.dispatch​Event 仍然會觸發完整的 event flow,而不僅僅觸發 event​Target​ 本身註冊的事件。

總的來說,閱讀源碼的過程是一次自我修煉的過程,是對過去某些不足的完善,其實就是發現自己很菜 😂,前方道險且長,同志們仍需努力呀 🤜。

不足

個人覺得閱讀優秀的源碼是一件很幸福的事,因爲它能潛移默化的提升你的審美能力。但同時我們也要帶有挑剔的眼光,找出當中存在的不足之處

fastClick 中 notNeeded 函數總的來說,已經相當不錯了,但是美中不足的是,對於 iOS 9.3 以上 使用 WKWebView 的用戶來說,引入 fastClick 無疑是多此一舉,還有可能導致某些潛在的問題。對於處女座的我來說,這一點是不能忍受的。不過單純的通過 UA 是無法區分 UIWebViewWKWebView 的。不過如果頁面是在自己 App 中話,可以通過在 UA 中攜帶 WebView的信息來決定是否加載

延遲檢測 code

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
    <style></style>
  </head>
  <body>
    <div>
      <label for="userAgent">userAgent:</label>
      <span id="userAgent"></span>
    </div>
    <div>
      <label for="touchstart">touchstart:</label>
      <span id="touchstart"></span>
    </div>
    <div>
      <label for="touchend">touchend:</label>
      <span id="touchend"></span>
    </div>
    <div>
      <label for="click">click:</label>
      <span id="click"></span>
    </div>
    <div>
      <label for="diffClickTouchend">diff click - touchend:</label>
      <span id="diffClickTouchend"></span>
    </div>
    <div>
      <div id="test">test</div>
      <div id="diff">diff</div>
    </div>
    <script>
      var userAgent = document.getElementById('userAgent');
      userAgent.innerText = window.navigator.userAgent;

      var test = document.getElementById('test');
      var diff = document.getElementById('diff');
      var touchstart = document.getElementById('touchstart');
      var touchend = document.getElementById('touchend');
      var click = document.getElementById('click');
      var diffClickTouchend = document.getElementById('diffClickTouchend');

      test.addEventListener('touchstart', function(e) {
        touchstart.innerText = Date.now();
      });

      test.addEventListener('touchend', function(e) {
        touchend.innerText = Date.now();
      });

      test.addEventListener('click', function(e) {
        click.innerText = Date.now();
      });

      diff.addEventListener('click', function() {
        diffClickTouchend.innerText = click.innerText - touchend.innerText;
      });
    </script>
  </body>
</html>

結束語

回眸歷史,不可否認 fastClick 在解決移動端 300ms 延遲的問題上的確作出傑出的貢獻,不過 9102 的今天,是否仍然有必要使用呢,回到開始,我說過,適合自己的纔是最好的,因此,如果你的業務需求,是只需要對 iOS 9.3 以上的 WKWebView 做適配,那麼強烈建議你不去使用,畢竟減少了文件請求大小,引入風險的概率。

最後,引用一句名言 老兵不死,只是凋零 向 fastClick 致敬。

參考 (References)


文 / lastSeries

作者也在掘金哦,快關注他吧!

編 / 熒聲

本文由創宇前端作者授權發佈,版權屬於作者,創宇前端出品。 歡迎註明出處轉載本文。文章鏈接:https://juejin.im/post/5cdf84...

本文是創宇前端相關賬號最後一次由熒聲負責。終於,我們一起走到了一個故事的結束。

都知歡聚最難得,難奈別離多

感謝您的閱讀,以及長期以來的支持。

再見啦。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章