移動端h5頁面touch事件與點擊穿透問題


前言

做過移動端H5頁面的同學肯定知道,移動端web的事件模型不同於PC頁面的事件。看了一些關於touch事件的文章,我想再來回顧下touch事件的原理,為什么通過touch可以觸發click事件,touch事件是不是萬能的以及它可能存在的問題。

touch事件的來源

PC網頁上的大部分操作都是用鼠標的,即響應的是鼠標事件,包括mousedownmouseupmousemoveclick事件。一次點擊行為,可被拆解成:mousedown -> click -> mouseup三步。

手機上沒有鼠標,所以就用觸摸事件去實現類似的功能。touch事件包含touchstarttouchmovetouchend,注意手機上並沒有tap事件。手指觸發觸摸事件的過程為:touchstart -> touchmove -> touchend

手機上沒有鼠標,但不代表手機不能響應mouse事件(其實是借助touch去觸發mouse事件)。有人在PC和手機上對事件做了對比實驗,以說明手機對touch事件相應速度快於mouse事件。

可以看到在手機上,當我們手觸碰屏幕時,要過300ms左右才會觸發mousedown事件,所以click事件在手機上看起來就像慢半拍一樣。

touch事件中可以獲取以下參數

參數 含義
touches 屏幕中每根手指信息列表
targetTouches 和touches類似,把同一節點的手指信息過濾掉
changedTouches 響應當前事件的每根手指的信息列表

tap是怎么來的

用過Zepto或KISSY等移動端js庫的人肯定對tap事件不陌生,我們做PC頁面時綁定click,相應地手機頁面就綁定tap。但原生的touch事件本身是沒有tap的,js庫里提供的tap事件都是模擬出來的。

我們在上面看到,手機上響應 click 事件會有300ms的延遲,那么這300ms到底是干嘛了?瀏覽器在 touchend 后會等待約300ms,原因是判斷用戶是否有雙擊(double tap)行為。如果沒有 tap 行為,則觸發 click 事件,而雙擊過程中就不適合觸發 click 事件了。由此可以看出 click 事件觸發代表一輪觸摸事件的結束。

既然說tap事件是模擬出來的,我們可以看下Zepto對 singleTap 事件的處理。見源碼 136-143 行,可以看出在 touchend 響應 250ms 無操作后,則觸發singleTap。

點擊穿透的場景

有了以上的基礎,我們就可以理解為什么會出現點擊穿透現象了。我們經常會看到“彈窗/浮層”這種東西,我做個了個demo。

整個容器里有一個底層元素的div,和一個彈出層div,為了讓彈出層有模態框的效果,我又加了一個遮罩層。

<div class="container"> <div id="underLayer">底層元素</div> <div id="popupLayer"> <div class="layer-title">彈出層</div> <div class="layer-action"> <button class="btn" id="closePopup">關閉</button> </div> </div> </div> <div id="bgMask"></div> 

然后為底層元素綁定 click 事件,而彈出層的關閉按鈕綁定 tap 事件。

$('#closePopup').on('tap', function(e){ $('#popupLayer').hide(); $('#bgMask').hide(); }); $('#underLayer').on('click', function(){ alert('underLayer clicked'); }); 

點擊關閉按鈕,touchend首先觸發tap,彈出層和遮罩就被隱藏了。touchend后繼續等待300ms發現沒有其他行為了,則繼續觸發click,由於這時彈出層已經消失,所以當前click事件的target就在底層元素上,於是就alert內容。整個事件觸發過程為 touchend -> tap -> click。

而由於click事件的滯后性(300ms),在這300ms內上層元素隱藏或消失了,下層同樣位置的DOM元素觸發了click事件(如果是input框則會觸發focus事件),看起來就像點擊的target“穿透”到下層去了。

完整demo請用chrome手機模擬器查看,或直接掃描二維碼在手機上查看。

結合Zepto源碼的解釋

zepto中的 tap 通過兼聽綁定在 document 上的 touch 事件來完成 tap 事件的模擬的,是通過事件冒泡實現的。在點擊完成時(touchstart / touchend)的 tap 事件需要冒泡到 document 上才會觸發。而在冒泡到 document 之前,手指接觸和離開屏幕(touchstart / touchend)是會觸發 click 事件的。

因為 click 事件有延遲(大概是300ms,為了實現safari的雙擊事件的設計),所以在執行完 tap 事件之后,彈出層立馬就隱藏了,此時 click 事件還在延遲的 300ms 之中。當 300ms 到來的時候,click 到的其實是隱藏元素下方的元素。

如果正下方的元素有綁定 click 事件,此時便會觸發,如果沒有綁定 click 事件的話就當沒發生。如果正下方的是 input 輸入框(或是 select / radio / checkbox),點擊默認 focus 而彈出輸入鍵盤,也就出現了上面的“點透”現象。

穿透的解決辦法

1. 遮擋

由於 click 事件的滯后性,在這段時間內原來點擊的元素消失了,於是便“穿透”了。因此我們順着這個思路就想到,可以給元素的消失做一個fade效果,類似jQuery里的fadeOut,並設置動畫duration大於300ms,這樣當延遲的 click 觸發時,就不會“穿透”到下方的元素了。

同樣的道理,不用延時動畫,我們還可以動態地在觸摸位置生成一個透明的元素,這樣當上層元素消失而延遲的click來到時,它點擊到的是那個透明的元素,也不會“穿透”到底下。在一定的timeout后再將生成的透明元素移除。具體可見demo

2. pointer-events

pointer-events是CSS3中的屬性,它有很多取值,有用的主要是autonone,其他屬性值為SVG服務。

取值 含義
auto 效果和沒有定義 pointer-events 屬性相同,鼠標不會穿透當前層。
none 元素不再是鼠標事件的目標,鼠標不再監聽當前層而去監聽下面的層中的元素。但是如果它的子元素設置了pointer-events為其它值,比如auto,鼠標還是會監聽這個子元素的。

關於使用 pointer-events 后的事件冒泡,有人做了個實驗,見代碼

因此解決“穿透”的辦法就很簡單,demo如下

$('#closePopup').on('tap', function(e){ $('#popupLayer').hide(); $('#bgMask').hide(); $('#underLayer').css('pointer-events', 'none'); setTimeout(function(){ $('#underLayer').css('pointer-events', 'auto'); }, 400); }); 

3. fastclick

使用fastclick庫,其實現思路是,取消 click 事件(參看源碼 164-173 行),用 touchend 模擬快速點擊行為(參看源碼 521-610 行)。

FastClick.attach(document.body); 

從此所有點擊事件都使用click,不會出現“穿透”的問題,並且沒有300ms的延遲。解決穿透的demo

有人(葉小釵)對事件機制做了詳細的剖析,循循善誘,並剖析了fastclick的源碼以自己模擬事件的創建。請看這篇文章,看完后一定會對移動端的事件有更深的了解


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM