移動設備的流行,帶動了移動互聯網的快速發展,不少開發者開始進入移動開發領域。目前市面上主流的移動設備通常都使用觸摸屏,觸摸屏所使用的觸摸事件模型與傳統網頁的鼠標事件模型有所區別,這種差別每每使初涉移動端的開發工程師陷入困境,事件穿透問題即是其中一個,本文將帶你瞭解事件穿透及如何在實際項目中選擇合適的方案解決事件穿透問題。css
當今,主流的移動設備通常都使用觸摸屏,Web 應用程序可使用觸摸事件(Touch Events)直接處理基於觸摸的輸入,或者應用程序可使用可解釋的鼠標事件以處理應用程序的輸入。使用鼠標事件的缺點是它們不支持併發用戶輸入,而觸摸事件支持多個同時輸入(可能在觸摸面上的不一樣位置),從而加強用戶體驗。html
觸摸事件有如下事件類型:node
在不少狀況下,觸摸事件和鼠標事件會同時被觸發(目的是讓沒有對觸摸設備優化的代碼仍然能夠在觸摸設備上正常工做)。以下代碼:併發
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 事件。設計
常見的事件穿透場景:code
注意:a 標籤的連接跳轉事件屬於 click 事件。htm
市面上解決事件穿透的方法有不少,大體能夠分爲兩類:第一種是禁止混用 click 和 touch 兩種事件;另外一種是延遲元素的隱藏或移除。事件
這種方法是將頁面內全部元素的 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 事件改用 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 事件的方法。