移動端300ms點擊延遲和點擊穿透

這個不是我原創,整理的資料方便學習積累知識

移動端300ms點擊延遲由來:

故事:2007 年初。蘋果公司在發佈首款 iPhone 前夕,遇到一個問題:當時的網站都是爲大屏幕設備所設計的。因而蘋果的工程師們作了一些約定,應對 iPhone 這種小屏幕瀏覽桌面端站點的問題。html

這當中最出名的,當屬雙擊縮放(double tap to zoom),這也是會有上述 300 毫秒延遲的主要緣由。git

雙擊縮放,顧名思義,即用手指在屏幕上快速點擊兩次,iOS 自帶的 Safari 瀏覽器會將網頁縮放至原始比例。 那麼這和 300 毫秒延遲有什麼聯繫呢? 假定這麼一個場景。用戶在 iOS Safari 裏邊點擊了一個連接。因爲用戶能夠進行雙擊縮放或者雙擊滾動的操做,當用戶一次點擊屏幕以後,瀏覽器並不能馬上判斷用戶是確實要打開這個連接,仍是想要進行雙擊操做。所以,iOS Safari 就等待 300 毫秒,以判斷用戶是否再次點擊了屏幕。 鑑於iPhone的成功,其餘移動瀏覽器都複製了 iPhone Safari 瀏覽器的多數約定,包括雙擊縮放,幾乎如今全部的移動端瀏覽器都有這個功能。以前人們剛剛接觸移動端的頁面,在欣喜的時候每每不會care這個300ms的延時問題,但是現在touch端界面如雨後春筍,用戶對體驗的要求也更高,這300ms帶來的卡頓慢慢變得讓人難以接受。github

也就是說,移動端瀏覽器會有一些默認的行爲,好比雙擊縮放、雙擊滾動。這些行爲,尤爲是雙擊縮放,主要是爲桌面網站在移動端的瀏覽體驗設計的。而在用戶對頁面進行操做的時候,移動端瀏覽器會優先判斷用戶是否要觸發默認的行爲。瀏覽器

重點:因爲移動端會有雙擊縮放的這個操做,所以瀏覽器在click以後要等待300ms,看用戶有沒有下一次點擊,也就是此次操做是否是雙擊。bash

瀏覽器開發商的解決方案

方案一:禁用縮放
當HTML文檔頭部包含以下meta標籤時:app

<meta name="viewport" content="user-scalable=no">
<meta name="viewport" content="initial-scale=1,maximum-scale=1">
複製代碼

代表這個頁面是不可縮放的,那雙擊縮放的功能就沒有意義了,此時瀏覽器能夠禁用默認的雙擊縮放行爲而且去掉300ms的點擊延遲。函數

缺點:就是必須經過徹底禁用縮放來達到去掉點擊延遲的目的,然而徹底禁用縮放並非咱們的初衷,咱們只是想禁掉默認的雙擊縮放行爲,這樣就不用等待300ms來判斷當前操做是不是雙擊。可是一般狀況下,咱們仍是但願頁面能經過雙指縮放來進行縮放操做,好比放大一張圖片,放大一段很小的文字。

方案二:更改默認的視口寬度

一開始,爲了讓桌面站點能在移動端瀏覽器正常顯示,移動端瀏覽器默認的視口寬度並不等於設備瀏覽器視窗寬度,而是要比設備瀏覽器視窗寬度大,一般是980px。咱們能夠經過如下標籤來設置視口寬度爲設備寬度。學習

<meta name="viewport" content="width=device-width">
複製代碼

由於雙擊縮放主要是用來改善桌面站點在移動端瀏覽體驗的,而隨着響應式設計的普及,不少站點都已經對移動端坐過適配和優化了,這個時候就不須要雙擊縮放了,若是可以識別出一個網站是響應式的網站,那麼移動端瀏覽器就能夠自動禁掉默認的雙擊縮放行爲而且去掉300ms的點擊延遲。若是設置了上述meta標籤,那瀏覽器就能夠認爲該網站已經對移動端作過了適配和優化,就無需雙擊縮放操做了。
這個方案相比方案一的好處在於,它沒有徹底禁用縮放,而只是禁用了瀏覽器默認的雙擊縮放行爲,但用戶仍然能夠經過雙指縮放操做來縮放頁面。測試

方案三:CSS touch-action

touch-action這個CSS屬性。這個屬性指定了相應元素上可以觸發的用戶代理(也就是瀏覽器)的默認行爲。若是將該屬性值設置爲touch-action: none,那麼表示在該元素上的操做不會觸發用戶代理的任何默認行爲,就無需進行300ms的延遲判斷。
優化

現有的解決方案

方案一:指針事件的polyfill
如今除了IE,其餘大部分瀏覽器都還不支持指針事件。有一些JS庫,可讓咱們提早使用指針事件,好比

然而,咱們如今關心的不是指針事件,而是與300ms延遲相關的CSS屬性touch-action。因爲除了IE以外的大部分瀏覽器都不支持這個新的CSS屬性,因此這些指針事件的polyfill必須經過某種方式去模擬支持這個屬性。一種方案是JS去請求解析全部的樣式表,另外一種方案是將touch-action做爲html標籤的屬性。

方案二:FastClick
FastClickFT Labs 專門爲解決移動端瀏覽器 300 毫秒點擊延遲問題所開發的一個輕量級的庫。FastClick的實現原理是在檢測到touchend事件的時候,會經過DOM自定義事件當即出發模擬一個click事件,並把瀏覽器在300ms以後的click事件阻止掉。

2、點擊穿透問題

說完移動端點擊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標籤,而後就發生連續跳轉了。。。諸如此類的,都是點擊穿透問題

解決方案:

  1. 只用touch

    最簡單的解決方案,完美解決點擊穿透問題

    把頁面內全部click所有換成touch事件(touchstart、’touchend’、’tap’),

    須要特別注意
    a標籤,a標籤的href也是click,須要去掉換成js控制的跳轉,或者直接改爲span + tap控制跳轉。若是要求不高,不在意滑走或者滑進來觸發事件的話,span + touchend就能夠了,畢竟tap須要引入第三方庫

    不用a標籤其實沒什麼,移動app開發不用考慮SEO,即使用了a標籤,通常也會去掉全部默認樣式,不如直接用span

  2. 只用click

    下下策
    ,由於會帶來300ms延遲,頁面內任何一個自定義交互都將增長300毫秒延遲,想一想都慢

    不用touch就不會存在touch以後300ms觸發click的問題,若是交互性要求不高能夠這麼作,

    強烈不推薦
    ,快一點老是好的
  3. tap後延遲350ms再隱藏mask

    改動最小,缺點是隱藏mask變慢了,350ms仍是能感受到慢的

    只須要針對mask作處理就行,改動很是小,若是要求不高的話,用這個比較省力

  4. pointer-events

    比較麻煩且有缺陷,

    不建議使用

    mask隱藏後,給按鈕下面元素添上pointer-events: none;樣式,讓click穿過去,350ms後去掉這個樣式,恢復響應

    缺陷是mask消失後的的350ms內,用戶能夠看到按鈕下面的元素點着沒反應,若是用戶手速很快的話必定會發現

  5. 在下面元素的事件處理器裏作檢測(配合全局flag)

    比較麻煩,

    不建議使用:

    全局flag記錄按鈕點擊的位置(座標點),在下面元素的事件處理器裏判斷event的座標點,若是相同則是那個可惡的click,拒絕響應

    上面說的只是想法,沒測試過,實在不行就用記錄時間戳判斷,等待350ms,這樣就和pointer-events差很少

  6. 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() 能夠阻止多數狀況的點擊穿透

相關文章
相關標籤/搜索