移动端点击穿透之谜
Posted 恪愚
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了移动端点击穿透之谜相关的知识,希望对你有一定的参考价值。
依然先说结论:目前为止所有的点击问题都是移动端事件触发顺序混乱导致的!
关于捕获和冒泡→
我们首先要知道的是:当我们鼠标按下一个按钮时,并不是“点击了一个按钮”,而是在这个区域内,鼠标(上的按键)被按下,操作系统和浏览器把这个信息对应到了“按钮”所在区域并触发其逻辑。
事实上鼠标点击并没有位置信息,是操作系统一直在监听鼠标移动,根据累积的位移计算出来的坐标,将其传给浏览器。
那么,把这个坐标转换为具体的元素上的事件的过程,就可称作“捕获”。那“冒泡”呢?这个不好直观解释,但有一点想必你是明白的:当你按下电视开关时,你也按到了电视!
这就是很多文章会讲到的“冒泡过程由内向外,捕获过程由外向内”,或者说是“洋葱模型”。
还有一点就是:事件addEventListener
的第三个参数 true/false
,即为“是捕获/冒泡”。(别多想,这只是浏览器提供的事件模型之一。无论是否监听,在一个事件发生时,捕获和冒泡总是先后发生的)
点击穿透
touch
事件结束后会默认触发元素的 click
事件,如没有设置完美视口,则事件触发的时间间隔为 300ms 左右,如设置完美视口则时间间隔为 30ms 左右(备注:具体的时间也看设备的特性)。
如果 touch 事件隐藏了元素,则 click 动作将作用到新的元素上,触发新元素的 click 事件或页面跳转,此现象称为点击穿透。
点击穿透会导致什么问题?
最常见的情况可能也就是发生在弹层中了 —— 你点击某一个元素结果触发了下面元素的事件,导致某些“诡异”现象的发生:
(以下截自我司微店APP某项目,代码已做脱敏处理)
在这个场景中可以看到:我在点开小黑弹框的时候在其层级之下、列表卡片组件层级之上新增了一个全屏弹层,试图让触摸到弹层的时候就关掉弹层和小黑弹框 —— 以便让再次点击或唤起其余弹框。
这里笔者封装的卡片组件中模仿elementUI给弹框加了‘底部校验’,即弹框的展开并不一定是在下部。这也就导致了“唤起以后不管他只有在再次点击‘管理’按钮时才消失”这个想法不能使用,因为会导致重叠!
通过上面的描述你可能感觉到了一丝问题:“触摸到弹层”意味着我使用的不是click
事件!
<div class="single-card" ref="singleRef">
<div class="single-mkt-card">
<!-- 一些结构 -->
<ul class="single-controller">
<li class="single-c-control" :class="'single-li': list_data.status===2" @click="handleControl">管理
<div class="single-c-tip" :class="singleTop" v-show="isBindControl">
<!-- 弹框里的结构 -->
<slot name="cardEdit" />
</div>
</li>
<li :class="xxx" @click="xxx">预览</li>
<li :class="xxx" @click="xxx">数据</li>
<li :class="xxx" v-if="list_data.status !== 2" @click="xxx"><i class="single-icon"></i>推广</li>
</ul>
</div>
<!-- bg弹层 -->
<div v-show="isBindControl" class="single-fx-tip" @touchstart="handleControlEdit"></div>
</div>
handleControl()
if(this.$refs.singleRef.getBoundingClientRect().bottom > 515)
this.isBoundingTop = true
else
this.isBoundingTop = false
this.isBindControl = !this.isBindControl;
,
handleControlEdit(e)
this.isBindControl = false;
,
结合上面说的“事件捕获”和“事件冒泡”,你应该也想到了这样一条过程:为了用户体验,在触摸到弹层的时候,弹层已经消失了;然后手指按到了别的按钮,移动端浏览器执行了click事件,于是别的按钮绑定的事件被触发了,你以为的“bug”就产生了。
怎么解决
点击穿透的产生原因既然是“移动端事件触发顺序”。那么就分为几种情况:
- 不同事件挂载在不同&非父子祖孙元素上,但是这些元素之间有关联
- 不同事件挂载在相同/父子祖孙元素上
- 相同事件作用在父子祖孙元素上
方案1:css pointer-events属性
css3的pointer-events
属性在这方面可算是“风光独盛”。当值为none
时表示禁止穿透。当绑定元素的后代元素的pointer-events
属性指定其他值时,鼠标事件可以指向后代元素,在这种情况下,鼠标事件将在捕获或冒泡阶段触发父元素的事件侦听器。
.single-fx-tip
pointer-events: none;
但很显然。这种方式不能作用在上面第一个场景中,因为事件的发生顺序问题,它并不能很好的监听到(但是这个属性在别的场景下非常有用!)。那么我们可以用另一种“取巧的方法”实现:
方案2:模拟click-300ms
上面提到了“设置完美视口”,也就是移动端常用的
<meta name="viewport" content="width=device-width">
如果没有,因为移动端要判断是否是双击,所以单击之后不能立刻触发click,要等上个300ms,知道确认了是不是双击 —— 事实上,即使设置了,也会有一定的延迟!
所以我们可以主动让元素在触发了某个事件后延迟 300ms 再走下面的流程:
handleControlEdit(e)
setTimeout(()=>
this.xxx = false; // 这时候就需要给浮层换一个控制变量了
,300)
,
方案3:阻止默认行为
除了上面说的“touch
事件结束后会默认触发元素的 click
事件”,还有就是,在微信自带的浏览器中,有一个“触顶下拉回弹”的操作,这其实是不应该的。它也属于浏览器默认事件。
一般我们需要禁止这种行为:
handleControlEdit(e)
this.isBindControl = false;
,
在原生代码中,考虑到兼容性问题,可以这么写:
// 全局阻止浏览器默认行为
document.addEventListener("touchstart",function(e)
//xxx
if(e.cancelable)
e.preventDefault();
,passive: false)
如果你用了“事件代理”,则可以使用
e.stopPropagation();
阻止事件冒泡。
点击和页面跳转
移动端页面跳转可以使用 a 链接,也可以使用 touchstart
事件来触发 JS 代码完成跳转
- 效率上,touchstart 速度更快
- SEO 优化上, a 链接效果更好
尤其是在webview中,不必关注SEO效果,除非有一个统一且硬性的规范,否则,我个人还是推荐第一个的。
以上是关于移动端点击穿透之谜的主要内容,如果未能解决你的问题,请参考以下文章
cocos2dx 3.X刚体update穿透问题。刚体A在update中通过摇杆移动,设置的和刚体
cocos2dx 3.X刚体update穿透问题。刚体A在update中通过摇杆移动,设置的和刚体