# 移动端点击穿透
# 问题引入
- 页面上有一个元素 A 与 B,层级关系是 B 在 A 的上面,但不是嵌套关系;
- 在 B 元素上注册
touchstart
事件,作用是隐藏 B 元素;A 元素上注册有click
事件(或者为 a 标签); - 问题是:B 元素隐藏后,会将一个事件强行加给下层的 A 元素,发生点击穿透。
# 原因
触屏设备为了区分用户单双击、缩放,对
click
做了 300ms 延迟触发,这些事件在移动端相应流程为:touchstart ==> touchmove ==> touchend ==> click
;当触发 B 的
touchstart
事件时,上层 B 隐藏;而 300ms 后,看用户在此时间内是否再次触摸屏幕,如果没有 300ms 后,此时弹窗已消失,浏览器在用户手指离开的位置触发
click
事件,所以点到了页面上原本在下层的 B 元素。
# 看一个例子

点击 B 相对于 A 的差集部分,只出触发 B 的 touchstart
。
代码如下:
<div id="A"></div>
<div id="B"></div>
<script>
const A = document.getElementById('A');
const B = document.getElementById('B');
A.addEventListener('click', function() {
console.log('糟糕,事件穿透了');
});
B.addEventListener('touchstart', function() {
console.log('上层 B 的 touchstart 触发');
B.style.display = 'none';
});
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
情况来了,当点击 A 于 B 的交集部分,发生了穿透,将 A 的 click
事件也一并触发了。

# 解决方案
# 1. 阻止事件冒泡
在 touch
阶段通过 e.preventDefault()
来阻止后面的 click
触发
B.addEventListener('touchstart', function(event) {
event.preventDefault();
console.log('上层 B 的 touchstart 触发');
B.style.display = 'none';
});
1
2
3
4
5
2
3
4
5
# 2. 使用延时器
延时 300ms 执行
B.addEventListener('touchstart', function() {
setTimeout(function() {
console.log('上层 B 的 touchstart 触发');
B.style.display = 'none';
}, 300);
});
1
2
3
4
5
6
2
3
4
5
6
# 3. 给上层元素增加动画
在触发 B touchstart
后,不让它立即消失,可以设置一个延时动画大于等于 300ms
$("#B").on("touchstart", function () {
$(this).fadeOut(300, function () {
$("#B").remove();
});
})
1
2
3
4
5
2
3
4
5
# 4. 使用 FastClick
使用 FastClick
库,本质是检测到 touchend
时,通过 DOM
自定义事件立即触发模拟一个 click
事件,并把浏览器在 300ms 之后真正的 click
事件阻止掉。