移动端点击穿透问题解决
移动端 touch 事件导致的点击穿透原因及解决方案
问题
移动端使用 touch 事件关闭弹窗或遮罩时,下层元素会意外触发点击事件,这就是点击穿透问题。
解答
穿透原因
移动端事件触发顺序:touchstart → touchmove → touchend → click
click 事件有约 300ms 延迟(用于判断双击),当 touchend 关闭遮罩后,延迟触发的 click 会作用到下层元素。
// 问题复现
mask.addEventListener('touchend', () => {
mask.style.display = 'none'; // 遮罩消失
// 300ms 后 click 事件触发,此时遮罩已不存在,点击穿透到下层
});
方案一:阻止默认行为
// 在 touchend 中阻止后续的 click 事件
mask.addEventListener('touchend', (e) => {
e.preventDefault();
mask.style.display = 'none';
});
方案二:延迟关闭
// 等 click 事件触发后再关闭
mask.addEventListener('touchend', () => {
setTimeout(() => {
mask.style.display = 'none';
}, 350); // 大于 300ms
});
方案三:使用 fastclick
// 消除 300ms 延迟,统一使用 click
import FastClick from 'fastclick';
FastClick.attach(document.body);
// 之后直接用 click 事件即可
mask.addEventListener('click', () => {
mask.style.display = 'none';
});
方案四:CSS pointer-events
mask.addEventListener('touchend', () => {
mask.style.display = 'none';
// 临时禁用下层元素的点击
underLayer.style.pointerEvents = 'none';
setTimeout(() => {
underLayer.style.pointerEvents = 'auto';
}, 350);
});
方案五:统一使用 touch 事件
// 所有交互都用 touch,不混用 click
function bindTap(el, callback) {
let startTime = 0;
let startX = 0;
let startY = 0;
el.addEventListener('touchstart', (e) => {
startTime = Date.now();
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
});
el.addEventListener('touchend', (e) => {
const endX = e.changedTouches[0].clientX;
const endY = e.changedTouches[0].clientY;
// 判断是点击而非滑动
if (
Date.now() - startTime < 300 &&
Math.abs(endX - startX) < 10 &&
Math.abs(endY - startY) < 10
) {
e.preventDefault();
callback(e);
}
});
}
// 使用
bindTap(mask, () => {
mask.style.display = 'none';
});
现代方案:touch-action
/* 禁用浏览器默认的 touch 行为,消除 300ms 延迟 */
html {
touch-action: manipulation;
}
// 现代浏览器可直接使用 click
mask.addEventListener('click', () => {
mask.style.display = 'none';
});
关键点
- 穿透原因:touch 和 click 之间有 300ms 延迟,遮罩消失后 click 作用到下层
e.preventDefault()可阻止 touchend 后的 click 触发- 现代浏览器用
touch-action: manipulation消除延迟是最简方案 - 避免 touch 和 click 混用,统一事件类型
- fastclick 已不再维护,现代项目推荐用 CSS 方案
目录