1. 程式人生 > >touch事件與點選穿透問題

touch事件與點選穿透問題

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

touch事件的來源

PC網頁上的大部分操作都是用滑鼠的,即響應的是滑鼠事件,包括mousedownmouseupmousemoveclick事件。一次點選行為,事件觸發過程為:mousedown -> mouseup -> click 三步。

手機上沒有滑鼠,所以就用觸控事件去實現類似的功能。touch事件包含touchstarttouchmove

touchend,注意手機上並沒有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.attach(document.body);

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

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

參考資料

點選穿透