移動設備的流行,帶動了移動互聯網的快速發展,很多開發者開始進入移動開發領域。目前市面上主流的移動設備一般都使用觸摸屏,觸摸屏所使用的觸摸事件模型與傳統網頁的滑鼠事件模型有所區別,這種差異往往使初涉移動端的開發工程師陷入困境,事件穿透問題便是其中一個,本文將帶你瞭解事件穿透及如何在實際項目中選擇合適的... ...
移動設備的流行,帶動了移動互聯網的快速發展,很多開發者開始進入移動開發領域。目前市面上主流的移動設備一般都使用觸摸屏,觸摸屏所使用的觸摸事件模型與傳統網頁的滑鼠事件模型有所區別,這種差異往往使初涉移動端的開發工程師陷入困境,事件穿透問題便是其中一個,本文將帶你瞭解事件穿透及如何在實際項目中選擇合適的方案解決事件穿透問題。
產生的原因
當今,主流的移動設備一般都使用觸摸屏,Web 應用程式可以使用觸摸事件(Touch Events)直接處理基於觸摸的輸入,或者應用程式可以使用可解釋的滑鼠事件以處理應用程式的輸入。使用滑鼠事件的缺點是它們不支持併發用戶輸入,而觸摸事件支持多個同時輸入(可能在觸摸面上的不同位置),從而增強用戶體驗。
觸摸事件有以下事件類型:
- touchstart:當觸摸點放置在觸摸面上時觸發。
- touchmove:當觸摸點沿觸摸錶面移動時觸發。
- touchend:當觸摸點從觸摸錶面移除時觸發。
- touchcancel:當觸摸點以實現特定的方式中斷(例如,創建的觸摸點太多)時觸發。
在很多情況下,觸摸事件和滑鼠事件會同時被觸發(目的是讓沒有對觸摸設備優化的代碼仍然可以在觸摸設備上正常工作)。如下代碼:
document.addEventListener('touchstart', () => {
console.log('touchstart')
})
document.addEventListener('touchend', () => {
console.log('touchend')
})
document.addEventListener('click', () => {
console.log('click')
})
事件觸發的先後順序是:touchstart -> touchend -> click。正是由於這種 click 事件的滯後性設計為事件穿透(點擊穿透)埋下了伏筆。
什麼是事件穿透
事件穿透是指觸發某個目標元素的觸摸事件時,會同時觸發該目標元素相同位置中其他元素的滑鼠點擊事件。例如:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>事件穿透</title>
<style>
* {
margin: 0;
padding: 0;
}
div {
width: 100vw;
height: 100vh;
line-height: 100vh;
text-align: center;
}
.mask {
position: fixed;
top: 0;
left: 0;
background: #333;
opacity: 0.6;
}
</style>
</head>
<body>
<div>事件穿透</div>
<div class="mask"></div>
<script>
const $div = document.querySelector("div")
const $mask = document.querySelector(".mask")
$mask.addEventListener('touchstart', (e) => {
console.log('mask touchstart')
e.target.style.display = 'none'
})
$div.addEventListener('click', () => {
console.log('div click')
})
</script>
</body>
</html>
由於 mask 元素觸發 touchstart 觸摸事件並立即隱藏掉自身,之後應該按先後順序觸發 mask 元素的 touchend 和 click 事件。然而,當要觸發 click 事件的時候由於 mask 元素已經隱藏掉了,於是觸發了 div 的 click 事件。
常見的事件穿透場景:
- 目標元素觸發觸摸事件時隱藏或移除自身,對應位置元素觸發 click 事件或 a 鏈接跳轉。
- 目標元素使用觸摸事件跳轉至新頁面,新頁面中對應位置元素觸發 click 事件或 a 鏈接跳轉。
註意:a 標簽的鏈接跳轉事件屬於 click 事件。
解決方法
市面上解決事件穿透的方法有很多,大致可以分為兩類:第一種是禁止混用 click 和 touch 兩種事件;另一種是延遲元素的隱藏或移除。
禁用 click 事件
這種方法是將頁面內所有元素的 click 事件改用 touch 事件。這種方法的好處非常明顯,既解決了 click 事件延遲造成體驗不佳的問題又解決了事件穿透的問題,但是缺點也很明顯,就是 a 標簽的鏈接跳轉的處理問題。
禁用 a 標簽的點擊事件,改用 touch 事件觸發鏈接跳轉。實現如下:
// 禁用 a 標簽的點擊事件
document.addEventListener('click', (e) => {
const href = e.target.getAttribute('href')
const nodeName = e.target.nodeName.toLowerCase()
if (nodeName === 'a' && href) {
e.preventDefault()
}
})
// 改用 touch 事件觸發鏈接跳轉
document.addEventListener('touchstart', (e) => {
const href = e.target.getAttribute('href')
const nodeName = e.target.nodeName.toLowerCase()
if (nodeName === 'a' && href) {
const target = e.target.getAttribute('target')
window.open(href, target || '_self')
}
})
看似很完美,然而,當 a 標簽內包含後帶元素的時候,後代元素的 click 事件通過冒泡還是會觸發 a 標簽的跳轉。怎麼解決?使用 pointer-events 禁用 a 標簽所有後代元素的滑鼠事件:
a[href] * {
pointer-events: none;
}
禁用 touch 事件
這種方法是將頁面內所有元素的 touch 事件改用 click 事件。事件穿透不就是由於 touch 與 click 事件存在觸發時間差造成的嗎,全部都使用 click 事件就不會有問題。然而事實真的如此美好?當然不是的,首先要解決 click 事件延遲 300ms 的問題。解決點擊事件延遲的問題可以使用以下的 CSS 代碼實現:
html {
touch-action: manipulation;
}
這樣已經很完美了。然而,什麼是工作?工作就是不停的解決問題。當你不得不為項目添加手勢功能,增加用戶體驗的時候(比如:左滑、右滑等等各種滑),你才會意識到完全禁用 touch 事件在實際項目中是不可能的事情。這個時候怎麼辦,推到從來,全部改用 touch 事件?當然不用這麼麻煩,你可以在使用 touch 事件時通過調用 preventDefault() 阻止觸發 click 事件。例如:
const $mask = document.querySelector(".mask")
$mask.addEventListener('touchstart', (e) => {
...
e.preventDefault()
})
總結
解決事件穿透還有通過設置動畫過渡延遲元素消失等方法,由於這類方法影響用戶體驗,不一一介紹。在實際項目開發中,純移動端項目優先推薦禁用 click 事件的方法,多端項目優先推薦禁用 touch 事件的方法。