移动端点击穿透之谜

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. 不同事件挂载在不同&非父子祖孙元素上,但是这些元素之间有关联
  2. 不同事件挂载在相同/父子祖孙元素上
  3. 相同事件作用在父子祖孙元素上

方案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效果,除非有一个统一且硬性的规范,否则,我个人还是推荐第一个的。

以上是关于移动端点击穿透之谜的主要内容,如果未能解决你的问题,请参考以下文章

移动端H5页面点击穿透问题

cocos2dx 3.X刚体update穿透问题。刚体A在update中通过摇杆移动,设置的和刚体

cocos2dx 3.X刚体update穿透问题。刚体A在update中通过摇杆移动,设置的和刚体

unity怎么让刚体不发生碰撞,直接穿过去

谈谈redis缓存击穿透和缓存击穿的区别,以及它们所引起的雪崩效应

Visual Studio 2013 Web 负载测试之谜