上週有幾天是在寫一個響應式網站,在寫到移動端交互時.遇到一個問題,就是點擊下拉框的選項時,下拉框背後的元素也被點擊了.其實這個就是著名的點擊穿透現象,所以趁着週末的時間把這個問題梳理了一下.而後呢,也是參考了一些文章以後整理了這篇總結,也算是本身對這個問題的一個記錄吧.全部參考連接都放在文末.你們有興趣的話,也能夠去看看.css
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
屬性,該設置會禁用掉該元素上的瀏覽器代理的任何默認行爲,包括縮放,移動,拖拽等.它把全部的觸摸類型的交互事件都禁止掉了,致使頁面也不能滾動.感受在稍微複雜點的實際開發中,應該不會這麼設置吧.github
html {
touch-action: none;
}
複製代碼
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
.下面咱們就先來介紹下這是個啥.函數
click事件是在最後執行的.通常狀況下,click是在手指放到屏幕上,而且沒有移動過,而後離開,且這個開始觸摸到手指離開屏幕的時間較短才能觸發,若手指移動了,則不會觸發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
事件也是有事件對象的.touchstart
和 touchmove
經過 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
,這是一個css3中的屬性.其中咱們用的比較多的屬性值有auto
和none
.其餘的屬性基本都是爲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-events
爲 none
,而後設置一個定時器,在必定的時間後將下層元素的屬性值改成auto
便可.
引入 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以及點擊穿透這樣的問題,那就索性研究記錄下你們的解決方案是啥.下次再遇到相似的問題基本就是成竹在胸了.下面是我參考的一些文章連接列表: