移动端点击穿透问题

移动端触摸事件触发 click 导致的点击穿透现象及解决方案

问题

移动端触摸元素后,约 300ms 会触发 click 事件,导致触发了下层元素的点击行为,这种现象称为点击穿透。

解答

发生原因

移动端触摸事件(touch)触发后,系统会延迟约 300ms 模拟产生 click 事件。如果触摸后上层元素消失,click 事件会作用到下层元素上。

触发条件

  • 上层元素监听了触摸事件,触摸后该元素消失
  • 下层元素具有点击特性(监听了 click 事件,或是 <a><input><button> 等标签)

常见场景

场景一:蒙层穿透

点击蒙层上的关闭按钮,蒙层消失后触发了下方元素的 click 事件。

场景二:触发链接跳转

点击按钮后元素消失,下方恰好是 <a> 标签,页面发生跳转。

场景三:跨页面穿透

点击按钮跳转到新页面,新页面中相同位置的元素 click 事件被触发。

场景四:连续跳转

新页面对应位置恰好是 <a> 标签,发生连续跳转。

解决方案

方案一:统一事件类型

不要混用 touch 和 click 事件,全部使用 touch 或全部使用 click。

// 只使用 click
button.addEventListener('click', () => {
  mask.style.display = 'none';
});

// 或只使用 touchend
button.addEventListener('touchend', (e) => {
  e.preventDefault(); // 阻止后续 click
  mask.style.display = 'none';
});

方案二:阻止默认行为

在 touchend 事件中调用 preventDefault()

button.addEventListener('touchend', (e) => {
  e.preventDefault();
  mask.style.display = 'none';
});

方案三:延迟隐藏

延迟 350ms 后再隐藏上层元素。

button.addEventListener('touchend', () => {
  setTimeout(() => {
    mask.style.display = 'none';
  }, 350);
});

方案四:pointer-events

临时禁用下层元素的点击。

button.addEventListener('touchend', () => {
  mask.style.display = 'none';
  document.body.style.pointerEvents = 'none';
  
  setTimeout(() => {
    document.body.style.pointerEvents = 'auto';
  }, 350);
});

方案五:遮挡层

用透明遮挡层拦截 click 事件。

button.addEventListener('touchend', () => {
  mask.style.display = 'none';
  
  const shield = document.createElement('div');
  shield.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;z-index:9999';
  document.body.appendChild(shield);
  
  setTimeout(() => {
    document.body.removeChild(shield);
  }, 350);
});

关键点

  • 点击穿透源于移动端 touch 事件后 300ms 触发 click 事件
  • 避免混用 touch 和 click 事件,统一使用一种事件类型
  • 使用 preventDefault() 阻止 touch 事件后的 click 触发
  • 可通过延迟隐藏、pointer-events 或遮挡层等方式消费掉多余的 click 事件