浏览器原理 · 38/51
1. addEventListener 第三个参数 2. addEventListener 与 attachEvent 区别 3. 浏览器兼容性测试与内核 4. 浏览器兼容性问题 5. 浏览器内核与引擎 6. 浏览器图层创建条件 7. 浏览器多进程架构 8. 浏览器渲染机制 9. 浏览器存储方案 10. 浏览器版本检测方法 11. children 与 childNodes 区别 12. 常见浏览器兼容性问题 13. Chrome 页面进程数量 14. 坐标系统对比 15. 多标签页通讯方案 16. 删除 Cookie 17. 自定义事件 18. DOM 事件处理方式演进 19. 元素尺寸属性对比 20. DOM 节点操作 21. DOM 事件机制 22. addEventListener 与 attachEvent 的区别 23. 获取页面所有复选框 24. HTMLCollection 与 NodeList 区别 25. Hybrid 应用开发 26. 强缓存命中机制 27. 浏览器缓存机制 28. 页面编码与资源编码不一致处理 29. jQuery 事件绑定方法对比 30. Input 点击触发的事件顺序 31. JavaScript 浏览器兼容性问题 32. jQuery 多事件绑定实现 33. JSBridge 原理 34. 链接点击后 Hover 失效解决方案 35. 减少重绘和回流的性能优化 36. 移动端 300ms 点击延迟问题 37. 移动端视口配置 38. 移动端点击穿透问题解决 39. 移动端兼容性问题 40. JSBridge 原理与实现 41. 移动端 1px 像素问题解决方案 42. 浏览器渲染流程 43. 页面加载完成事件对比 44. Offset、Scroll、Client 属性对比 45. 同源策略与跨域解决方案 46. Script 标签位置对页面加载的影响 47. Service Worker 与 PWA 48. 存储方案对比:Cookie、Storage、IndexedDB 49. 强缓存默认时间 50. URL 到页面显示的完整过程 51. V8 引擎 JavaScript 执行过程

移动端点击穿透问题解决

移动端 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 方案