移動端300ms以及點擊穿透

上週有幾天是在寫一個響應式網站,在寫到移動端交互時.遇到一個問題,就是點擊下拉框的選項時,下拉框背後的元素也被點擊了.其實這個就是著名的點擊穿透現象,所以趁着週末的時間把這個問題梳理了一下.而後呢,也是參考了一些文章以後整理了這篇總結,也算是本身對這個問題的一個記錄吧.全部參考連接都放在文末.你們有興趣的話,也能夠去看看.css

300ms延遲

延遲產生緣由

300ms 延遲的由來,是當初07年初蘋果發佈首款iPhone以前,蘋果工程師提出的一個爲了優化交互體驗的操做.由於當時的網站基本都是爲PC等大屏幕設備而寫的,而如今須要用小屏幕瀏覽桌面端網站.當用戶用手指把頁面放大之後,就有了一個雙擊縮放(double tap to zoom)的交互.即在iOS自帶的Safari瀏覽器中快速雙擊會將網頁縮放到原始比例.所以在此瀏覽器中,用戶會有單擊或者雙擊的需求行爲.而瀏覽器並不能馬上判斷用戶是想要單擊仍是雙擊.因而乎,Safari就等待了300ms.看看用戶到底想幹嗎.鑑於蘋果公司這個操做的成功,後續其餘的瀏覽器也所以借鑑了這種行爲,而300ms也所以成爲了大多數瀏覽器的一個約定.在當初移動端興起的時候,300ms是可讓人接受的,可是隨着用戶對交互體驗的要求愈來愈高,300ms就成爲了用戶沒法忍受的一個點了.html

解決延遲

上面簡單的介紹了下300ms延遲產生的緣由.那麼在移動端開發中,該怎麼解決click事件的300ms延遲呢?目前網上的解決方案也較多,我也按照那些方案作了下測試,整理以下:vue

禁用縮放

禁用瀏覽器的縮放,就是給咱們的頁面裏面加個meta頭部,代表這個網頁是不可縮放的.在App Store下載了幾個瀏覽器試了下,夸克瀏覽器和QQ瀏覽器測試是有效果的.Safari,Chrome,Firefox 都不行.這個測試結果和我在一些文章裏面看的不同,這是爲啥啊?css3

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

改變視口寬度

更改默認的視口寬度,這下除了safari瀏覽器還有300ms之外,其餘的瀏覽器都已經沒有延遲了.貌似是由於這個方案尚未被safari經過.由於咱們已經爲用戶適配了頁面大小和阻止了用戶縮放,因此瀏覽器就不用再判斷用戶雙擊縮放了,因而便自動取消了click事件的300ms延遲git

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

touch-action

設置 touch-action 屬性,該設置會禁用掉該元素上的瀏覽器代理的任何默認行爲,包括縮放,移動,拖拽等.它把全部的觸摸類型的交互事件都禁止掉了,致使頁面也不能滾動.感受在稍微複雜點的實際開發中,應該不會這麼設置吧.github

html {
  touch-action: none;
}
複製代碼

引用fastclick庫

1fastclick原理: 在檢測到 touchend 事件的時候,經過DOM自定義事件當即模擬一個click事件,並把瀏覽器本來300ms以後的click事件阻止掉.缺點是腳本相對較大,且有時候可能會有bug. 項目地址 : fastclick瀏覽器

既然click事件屁事這麼多,那能不能用touch事件來代替click事件呢?答案是不能.假如咱們用 touchstart 事件來代替,一方面在用戶手指觸摸屏幕的時候就能觸發,但有時候用戶並不想點擊,只想單純的滑動屏幕而已.另外一方面,就是在某些場景下可能會出現點擊穿透現象(ghost click).app

點擊穿透

什麼是點擊穿透

頁面倆元素A和B,B在A的上面.在B的上面註冊了touchstart事件,回調函數中是讓B元素隱藏.當咱們點擊B元素的時候,除了B被隱藏外,A的click事件也被觸發了.這是由於在移動端瀏覽器中,事件執行順序是 touchstart => touchmove => touchend => click .click是有300ms的延遲.當B的touchstart事件觸發後,B被隱藏了,300ms以後,瀏覽器觸發了click事件,此時的事件已經被派發到了A元素身上.框架

事件執行順序

移動端的事件是touch事件,也叫觸摸事件,由於是用手指觸摸的.固然,點擊事件仍是存在的. 咱們在上面提到了,在移動端瀏覽器中,事件執行順序是 touchstart => touchmove => touchend => click.下面咱們就先來介紹下這是個啥.函數

  • touchstart 是手指放到屏幕上就觸發
  • touchmove 是手指在屏幕上滑動的時候觸發
  • touchend 是手指離開屏幕的時候觸發

click事件是在最後執行的.通常狀況下,click是在手指放到屏幕上,而且沒有移動過,而後離開,且這個開始觸摸到手指離開屏幕的時間較短才能觸發,若手指移動了,則不會觸發click事件了(看到有的地方說某些瀏覽器會容許有一個很小的移動值,具體的狀況不太清楚). 所以正確的觸發順序是下面兩個中的一個:

  • touchstart => touchmove => touchend
  • touchstart => touchend => click

touchmove,可能不會觸發,也可能觸發不少次,若觸發了touchmove,則click就不會觸發了. 咱們在Chrome瀏覽器的手機模式下運行下面的代碼來驗證上面的結論.

<<button id="btn">click me</button>
<div id="app"></div>
複製代碼
let btn = document.querySelector('#btn')
let app = document.querySelector('#app')
let s = ''

btn.addEventListener('click', function(){
  s += 'click '
  app.innerText = s
})

btn.addEventListener('touchstart', function(e){
  s += 'touchstart '
  app.innerText = s
  // e.preventDefault()
})

btn.addEventListener('touchmove', function(){
  s += 'touchmove '
  app.innerText = s
})

btn.addEventListener('touchend', function(){
  s += 'touchend '
  app.innerText = s
})
複製代碼

咱們在按鈕上單純的點擊一下,會打印出touchstart touchend click.在按鈕上快速的從左滑到右,會打印出touchstart touchmove touchmove touchmove touchmove touchend,這裏的touchmove的數量不定.

click 等事件同樣,touch 事件也是有事件對象的.touchstarttouchmove 經過 event.touched 來獲取手指信息(好比觸摸的點的位置等信息).可是 touchend 不能經過 event.touched 來獲取.由於此時的手指已經離開了.可是能夠經過 event.changedTouches 來獲取手指信息.

解決點擊穿透

解決點擊穿透的方案也有好幾個,你們能夠根據本身項目的實際狀況選擇對應的解決方案.

元素阻擋

新增一個看不見的元素阻止事件穿透.解決思路基本就是在觸發事件的位置動態生成一個新的透明元素,這樣當300ms以後的click事件來臨時,點到的就是這個透明元素,而後再把這個元素刪除便可.缺點就是寫法麻煩,並且有時候用戶快速點擊的時候,下面元素的事件有可能不會觸發,由於此時的透明元素尚未被定時器清理掉.固然還有一個方案和這個很像,那就是弄一個隱藏動畫,時間大於300ms,在延遲以後的點擊事件來臨時,上面的元素尚未消失,這樣就不會點到下面的元素了.

<div class="div1"></div>
<div class="div2"></div>
複製代碼
.div1 {
  width: 200px;
  height: 200px;
  background-color: pink;
}
.div2 {
  width: 200px;
  height: 200px;
  background-color: orange;
  position: relative;
  top: -100px;
  display: block;
  opacity: 1;
}
複製代碼
let div1 = document.querySelector('.div1')
let div2 = document.querySelector('.div2')

div1.addEventListener('click',function(){
  console.log('div1')
})

div2.addEventListener('touchstart',function(e){
  console.log('div2')
  let el = document.createElement('div')
  el.style.width = '200px'
  el.style.height = '200px'
  el.style.opacity = '0'
  el.style.position = 'relative'
  el.style.top = '-100px'
  document.body.appendChild(el)
  this.style.display = 'none'
  setTimeout(() => {
    document.body.removeChild(el)
  }, 400)
})
複製代碼

阻止默認事件

使用 event.preventDefault() ,把上面的代碼改爲

div2.addEventListener('touchstart',function(){
  console.log('div2')
  this.style.display = 'none'
  e.preventDefault()
})
複製代碼

pointer-events

使用 pointer-events,這是一個css3中的屬性.其中咱們用的比較多的屬性值有autonone.其餘的屬性基本都是爲SVG服務的,戳我查看該屬性詳細介紹.

初始值就是auto,適用於全部的元素,其表現效果和 pointer-events 屬性未指定時同樣.鼠標不會穿透當前層. none 則該元素不會成爲鼠標事件的目標.鼠標再也不監聽當前層而去監聽下面層中的元素.可是若它的子元素設置了 pointer-events 爲其餘值,則鼠標仍是會監聽這個子元素的.運行下面代碼,觀察效果.

.div1 {
  width: 200px;
  height: 200px;
  background-color: orange;
}
.div2 {
  width: 100px;
  height: 100px;
  background-color: pink;
  pointer-events: none;
}
.div3 {
  width: 50px;
  height: 50px;
  background-color: #00BFFF;
  pointer-events: auto;
}
複製代碼
<div class="div1">
    div1
    <div class="div2">
        div2
        <div class="div3">
            div3
        </div>
    </div>
</div>
複製代碼
let div1 = document.querySelector('.div1')
let div2 = document.querySelector('.div2')
let div3 = document.querySelector('.div3')

div1.addEventListener('click',function(){
    console.log('div1')
})
div2.addEventListener('click',function(){
    console.log('div2')
})
div3.addEventListener('click',function(){
    console.log('div3')
})
複製代碼

因此這裏咱們解決點擊穿透的方法就是,先設置下層元素的 pointer-eventsnone,而後設置一個定時器,在必定的時間後將下層元素的屬性值改成auto 便可.

fastclick庫

引入 fastclick 庫.

<script src="https://lib.baomitu.com/fastclick/1.0.6/fastclick.min.js"></script>
複製代碼

下面是原生JS中的寫法,在不一樣的庫(如jQuery)或者框架(如vue)中的寫法都不太同樣.不過基本都是大同小異,網上一搜一大堆,你們用到的時候去搜索下就好了.

if('addEventListener' in document){
  document.addEventListener('DOMContentLoaded', function(){
    FastClick.attach(document.body)
  }, false)
}
複製代碼

總結

既然這回遇到了300ms以及點擊穿透這樣的問題,那就索性研究記錄下你們的解決方案是啥.下次再遇到相似的問題基本就是成竹在胸了.下面是我參考的一些文章連接列表:

相關文章
相關標籤/搜索