這個不是我原創,整理的資料方便學習積累知識
移動端300ms點選延遲由來:
故事:2007 年初。蘋果公司在釋出首款 iPhone 前夕,遇到一個問題:當時的網站都是為大螢幕裝置所設計的。於是蘋果的工程師們做了一些約定,應對 iPhone 這種小螢幕瀏覽桌面端站點的問題。
這當中最出名的,當屬雙擊縮放(double tap to zoom),這也是會有上述 300 毫秒延遲的主要原因。
雙擊縮放,顧名思義,即用手指在螢幕上快速點選兩次,iOS 自帶的 Safari 瀏覽器會將網頁縮放至原始比例。 那麼這和 300 毫秒延遲有什麼聯絡呢? 假定這麼一個場景。使用者在 iOS Safari 裡邊點選了一個連結。由於使用者可以進行雙擊縮放或者雙擊滾動的操作,當使用者一次點選螢幕之後,瀏覽器並不能立刻判斷使用者是確實要開啟這個連結,還是想要進行雙擊操作。因此,iOS Safari 就等待 300 毫秒,以判斷使用者是否再次點選了螢幕。 鑑於iPhone的成功,其他移動瀏覽器都複製了 iPhone Safari 瀏覽器的多數約定,包括雙擊縮放,幾乎現在所有的移動端瀏覽器都有這個功能。之前人們剛剛接觸移動端的頁面,在欣喜的時候往往不會care這個300ms的延時問題,可是如今touch端介面如雨後春筍,使用者對體驗的要求也更高,這300ms帶來的卡頓慢慢變得讓人難以接受。
也就是說,移動端瀏覽器會有一些預設的行為,比如雙擊縮放、雙擊滾動。這些行為,尤其是雙擊縮放,主要是為桌面網站在移動端的瀏覽體驗設計的。而在使用者對頁面進行操作的時候,移動端瀏覽器會優先判斷使用者是否要觸發預設的行為。
重點:由於移動端會有雙擊縮放的這個操作,因此瀏覽器在click之後要等待300ms,看使用者有沒有下一次點選,也就是這次操作是不是雙擊。
瀏覽器開發商的解決方案
方案一:禁用縮放
當HTML文件頭部包含如下meta
標籤時:
<
meta name="viewport" content="user-scalable=no">
<
meta name="viewport" content="initial-scale=1,maximum-scale=1">
複製程式碼
表明這個頁面是不可縮放的,那雙擊縮放的功能就沒有意義了,此時瀏覽器可以禁用預設的雙擊縮放行為並且去掉300ms的點選延遲。
方案二:更改預設的視口寬度
一開始,為了讓桌面站點能在移動端瀏覽器正常顯示,移動端瀏覽器預設的視口寬度並不等於裝置瀏覽器視窗寬度,而是要比裝置瀏覽器視窗寬度大,通常是980px。我們可以通過以下標籤來設定視口寬度為裝置寬度。
<
meta name="viewport" content="width=device-width">
複製程式碼
因為雙擊縮放主要是用來改善桌面站點在移動端瀏覽體驗的,而隨著響應式設計的普及,很多站點都已經對移動端坐過適配和優化了,這個時候就不需要雙擊縮放了,如果能夠識別出一個網站是響應式的網站,那麼移動端瀏覽器就可以自動禁掉預設的雙擊縮放行為並且去掉300ms的點選延遲。如果設定了上述meta
標籤,那瀏覽器就可以認為該網站已經對移動端做過了適配和優化,就無需雙擊縮放操作了。
這個方案相比方案一的好處在於,它沒有完全禁用縮放,而只是禁用了瀏覽器預設的雙擊縮放行為,但使用者仍然可以通過雙指縮放操作來縮放頁面。
方案三:CSS touch-action
touch-action
這個CSS屬性。這個屬性指定了相應元素上能夠觸發的使用者代理(也就是瀏覽器)的預設行為。如果將該屬性值設定為touch-action: none
,那麼表示在該元素上的操作不會觸發使用者代理的任何預設行為,就無需進行300ms的延遲判斷。
現有的解決方案
方案一:指標事件的polyfill
現在除了IE,其他大部分瀏覽器都還不支援指標事件。有一些JS庫,可以讓我們提前使用指標事件,比如
- Google 的 Polymer
- 微軟的 HandJS
- @Rich-Harris 的 Points
然而,我們現在關心的不是指標事件,而是與300ms延遲相關的CSS屬性touch-action
。由於除了IE之外的大部分瀏覽器都不支援這個新的CSS屬性,所以這些指標事件的polyfill必須通過某種方式去模擬支援這個屬性。一種方案是JS去請求解析所有的樣式表,另一種方案是將touch-action
作為html標籤的屬性。
方案二:FastClick
FastClick 是 FT Labs 專門為解決移動端瀏覽器 300 毫秒點選延遲問題所開發的一個輕量級的庫。FastClick的實現原理是在檢測到touchend事件的時候,會通過DOM自定義事件立即出發模擬一個click事件,並把瀏覽器在300ms之後的click事件阻止掉。
二、點選穿透問題
說完移動端點選300ms延遲的問題,還不得不提一下移動端點選穿透的問題。可能有人會想,既然click點選有300ms的延遲,那對於觸控式螢幕,我們直接監聽touchstart事件不就好了嗎?
使用touchstart去代替click事件有兩個不好的地方。
第一:touchstart是手指觸控螢幕就觸發,有時候使用者只是想滑動螢幕,卻觸發了touchstart事件,這不是我們想要的結果;
第二:使用touchstart事件在某些場景下可能會出現點選穿透的現象。
什麼是點選穿透?
假如頁面上有兩個元素A和B。B元素在A元素之上。我們在B元素的touchstart事件上註冊了一個回撥函式,該回撥函式的作用是隱藏B元素。我們發現,當我們點選B元素,B元素被隱藏了,隨後,A元素觸發了click事件。
這是因為在移動端瀏覽器,事件執行的順序是touchstart >
touchend >
click。而click事件有300ms的延遲,當touchstart事件把B元素隱藏之後,隔了300ms,瀏覽器觸發了click事件,但是此時B元素不見了,所以該事件被派發到了A元素身上。如果A元素是一個連結,那此時頁面就會意外地跳轉。
注:瀏覽器事件觸發的順序
touchstart –>
mouseover(有的瀏覽器沒有實現) –>
mousemove(一次) –>
mousedown –>
mouseup –>
click –>
touchend
Touch 事件中,常用的為 touchstart, touchmove, touchend 三種。除此之外還有touchcancel。 注意,原生事件中並沒有tap事件。下面會解釋tap事件怎麼產生的。
事件描述如下:
事件 | 描述 | 觸發時機 |
---|---|---|
touchstart | 開始觸控 | 手指接觸螢幕時立即觸發 |
touchmove | 移動或拖拽 | 取決於系統和瀏覽器 |
touchend | 觸控結束 | 手指離開螢幕時立即出發 |
而Touch事件的觸發一般通過手指,還會存在多點觸控,拖拽方向等情況。列出幾個重要引數如下:
引數 | 含義 |
---|---|
touches | 螢幕中每根手指資訊列表 |
targetTouches | 和touches類似,把同一節點的手指資訊過濾掉 |
changedTouches | 響應當前事件的每根手指的資訊列表 |
程式碼獲取如下:
elemenrRef.addEventListener('touchstart', function(e) {
console.log(e.touches, e.targetTouches, e.changedTouches);
});
複製程式碼
手指觸發觸控事件的過程如下:
touchstart -->
mouseover(有的瀏覽器沒有實現) -->
mousemove(一次) -->
mousedown -->
mouseup -->
click -->
touchend複製程式碼
由此,我們可以在 ontouchstart 事件上記錄開始觸控開始,ontouchend 記錄觸控結束資訊。 通過上述這些引數,很容易的去計算幽冥點選的時間,以及點選穿透的相關資訊,包括響應的座標情況。
現象:
1) 點選穿透問題:點選蒙層(mask)上的關閉按鈕,蒙層消失後發現觸發了按鈕下面元素的click事件,
蒙層的關閉按鈕繫結的是touch事件,而按鈕下面元素繫結的是click事件,touch事件觸發之後,蒙層消失了,300ms後這個點的click事件fire,event的target自然就是按鈕下面的元素,因為按鈕跟蒙層一起消失了
2) 跨頁面點選穿透問題:如果按鈕下面恰好是一個有href屬性的a標籤,那麼頁面就會發生跳轉
因為a標籤跳轉預設是click事件觸發,所以原理和上面的完全相同
3) 另一種跨頁面點選穿透問題:這次沒有mask了,直接點選頁內按鈕跳轉至新頁,然後發現新頁面中對應位置元素的click事件被觸發了
和蒙層的道理一樣,js控制頁面跳轉的邏輯如果是繫結在touch事件上的,而且新頁面中對應位置的元素繫結的是click事件,而且頁面在300ms內完成了跳轉,三個條件同時滿足,就出現這種情況了
非要細分的話還有第四種,不過概率很低,就是新頁面中對應位置元素恰好是a標籤,然後就發生連續跳轉了。。。諸如此類的,都是點選穿透問題
解決方案:
-
只用touch
最簡單的解決方案,完美解決點選穿透問題
把頁面內所有click全部換成touch事件(
touchstart
、’touchend’、’tap’),需要特別注意a標籤,a標籤的href也是click,需要去掉換成js控制的跳轉,或者直接改成span + tap控制跳轉。如果要求不高,不在乎滑走或者滑進來觸發事件的話,span + touchend就可以了,畢竟tap需要引入第三方庫
不用a標籤其實沒什麼,移動app開發不用考慮SEO,即便用了a標籤,一般也會去掉所有預設樣式,不如直接用span
-
只用click
下下策,因為會帶來300ms延遲,頁面內任何一個自定義互動都將增加300毫秒延遲,想想都慢
不用touch就不會存在touch之後300ms觸發click的問題,如果互動性要求不高可以這麼做,
強烈不推薦,快一點總是好的
-
tap後延遲350ms再隱藏mask
改動最小,缺點是隱藏mask變慢了,350ms還是能感覺到慢的
只需要針對mask做處理就行,改動非常小,如果要求不高的話,用這個比較省力
-
pointer-events
比較麻煩且有缺陷,
不建議使用mask隱藏後,給按鈕下面元素添上
pointer-events: none;
樣式,讓click穿過去,350ms後去掉這個樣式,恢復響應缺陷是mask消失後的的350ms內,使用者可以看到按鈕下面的元素點著沒反應,如果使用者手速很快的話一定會發現
-
在下面元素的事件處理器裡做檢測(配合全域性flag)
比較麻煩,
不建議使用:全域性flag記錄按鈕點選的位置(座標點),在下面元素的事件處理器裡判斷event的座標點,如果相同則是那個可惡的click,拒絕響應
上面說的只是想法,沒測試過,實在不行就用記錄時間戳判斷,等待350ms,這樣就和
pointer-events
差不多 -
fastclick
好用的解決方案,不介意多載入幾KB的話,
不建議使用:,因為有人遇到了bug,首先引入fastclick庫,再把頁面內所有touch事件都換成click,其實稍微有點麻煩,建議引入這幾KB就為了解決點透問題不值得,不如用第一種方法呢
注:程式碼上處理建議如下:
在touchend事件上呼叫 preventDefault()
在一次成功的點選後,建議接下來的 500ms 以內取消所有的 click 事件。
分析點選事件,判斷如果是慢速點選穿透,則取消所有 click 事件,如果是快速點選穿透,取消觸控事件 50ms以內的 click 事件即可。
別的參考思路(開源庫fastclick),取消 click 事件,用touchend 模擬 快速點選行為。
Why
問題來了,click 事件什麼時候觸發?
瀏覽器在 touchend 之後會等待約 300ms ,如果沒有 tap 行為,則觸發 click 事件。 而瀏覽器等待約 300ms 的原因是,判斷使用者是否是雙擊(double tap)行為,雙擊過程中就不適合觸發 click 事件了。 由此可以看出 click 事件觸發代表一輪觸控事件的結束。
上面說到原生事件中並沒有 tap 事件,可以參考經典的 zepto.js 對 singleTap 事件的處理(遺憾的是在部分瀏覽器中,依然存在點選穿透的問題)。可以看出,singleTap 事件的觸發時機 —— 在 touchend 事件響應 250ms 無操作後,觸發singleTap。因此,點選穿透的現象就容易理解了,在這 300ms 以內,因為上層元素隱藏或消失了,由於 click 事件的滯後性,同樣位置的 DOM 元素觸發了 click 事件(如果是 input 則觸發了 focus 事件)。在程式碼中,給我們的感覺就是 target 發生了飄移。
如何處理點選穿透(思路)
1. 觸控開始時 touchstart 事件觸發時,preventDefault()。毫無疑問,很容易想到這一點,而且也從根本上解決了這個問題。但是,它有一個避免不了或者說引入了很大的缺陷,頁面中DOM 元素無法再進行滾動了。這個方法顯然不能滿足我們的需求,但是這個思路其實可以給我們更多的啟發,比如說 iscroll 只允許橫向滾動的實現,相關實現這裡暫且不表。
2. 觸控結束時 touchend 事件觸發時,preventDefault()。看上去好像沒有什麼問題,但是,很遺憾的是不是所有的瀏覽器都支援。
3. 禁止頁面縮放 通過設定meta標籤,可以禁止頁面縮放,部分瀏覽器不再需要等待 300ms,導致點選穿透。點選事件仍然會觸發,但相對較快,所以 click 事件從某種意義上來說可以取代點選事件, 而代價是犧牲少數使用者(click 事件觸發仍然較慢)的體驗。
<
meta name="viewport" content="width=device-width, user-scalable=no">
複製程式碼
移動端chromiun 和 iOS 9.3+ 可以用 CSS 屬性來阻止元素的雙擊縮放進而取消點選穿透的延遲:
html {
-ms-touch-action: manipulation;
touch-action: manipulation;
} 複製程式碼
4. CSS3 的方法 雖然主要講的是事件,但是有必要介紹一個 CSS3 的屬性 —— pointer-events。
pointer-events: auto | none | visiblePainted | visibleFill | visibleStroke | visible | painted | fill | stroke | all | inherit;
複製程式碼
pointer-events 屬性有很多值,有用的主要是 auto 和 none,其他屬性為 SVG 服務。
可見移動端開發還是可以用的。
屬性 | 含義 |
---|---|
auto | 預設值,滑鼠或觸屏事件不會穿透當前層 |
none | 元素不再是target,監聽的元素變成了下層的元素(如果子元素設定成 auto,點選子元素會繼續監聽事件) |
5. 處理點選事件 —— Touch to Click 最靠譜的方案還是從點選事件的根源上解決問題。用 js 去判斷幽冥點選,然後阻止點選穿透。這種方式顯然可以實現,缺點是阻止點選穿透時需要小心,不要導致原生的 HTML 元素(如:連結,多選框,單選框)無法正常執行。
通過上文中介紹的 touches,targetTouches,changedTouches 引數,我們可以構建出這樣的測試頁面,可以統計出點選穿透的時間,以及已經響應的情況。
preventDefault() | 點選穿透時間 | 點選穿透區域 | ||||
---|---|---|---|---|---|---|
touchstart | touchend | 縮放頁面 | 禁止縮放頁面 | 縮放頁面 | 禁止縮放頁面 | |
Safari Mobile iOS 5.1.1 | Yes | Yes | 370ms after end | 370msafter end | touchstart | touchstart |
Safari Mobile iOS 6.1.3 | Yes | Yes | 370ms after end | 370msafter end | touchstart | touchstart |
Safari Mobile iOS 7.1.1 | Yes | Yes | 370ms after end | 370msafter end | touchstart | touchstart |
Android 2.3.7 | Yes | No | 410ms after end | 410msafter end | touchstart | touchstart |
Android 4.0.4 | Yes | No | 300ms after end | 10ms after end | touchstart | touchstart |
Android 4.1.2 | Yes | No | 300ms after end | 300msafter end | touchstart | touchstart |
Android 4.2.2 | Yes | No | 300ms after start | 10ms after end | touchstart | touchend |
IE10 Windows Phone 8 | No | No | 310ms after end | 10ms after end | touchend | touchend |
Blackberry 10 | Yes | Yes | 260ms after end | 10ms after end | touchstart | touchstart |
Chrome for iOS | Yes | Yes | 360ms after end | 360msafter end | touchstart | touchstart |
Chrome for Android | Yes | Yes | 300ms after start | 10ms after end | touchstart | touchend |
Firefox for Android | Yes | No | 300ms after end | 10ms after end | touchstart | touchend |
由此可以看出: 1. 點選穿透受瀏覽器和頁面是否縮放影響 2. 點選穿透有兩種情況:快速情況有 10ms 慢速情況有 300ms 3. 在 touchend 時間上呼叫 preventDefault() 可以阻止多數情況的點選穿透