移动端点击穿透问题解决

移动端 touch 事件导致的点击穿透原因及解决方案

问题

移动端使用 touch 事件关闭弹窗或遮罩时,下层元素会意外触发点击事件,这就是点击穿透问题。

解答

穿透原因

移动端事件触发顺序:touchstarttouchmovetouchendclick

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 方案