曝光移动端touch事件的若干坑

最近做一个插件:下拉刷新,实在是被touchevent坑的死去活来,记录下来帮助有幸看到的你。

一个简化的场景是这样的:

我在out元素上注册了事件处理函数,响应一系列的touchevent,包括touchstart/touchmove/touchend/touchcancel这几类事件。

我以为发生在out上的各种触摸事件,都应该可以回调给我,并且清晰的列举出当前out上有几根手指,可惜事情远没有这么简单,下面一一道来。

首先,看一下touchevent的结构:

重要的几个字段:

  • touches:表示屏幕上所有的手指,无论这个手指是不是在out上。
  • targetTouches:表示在out内的手指。
  • changedTouches:表示屏幕上所有发生过变化的手指。
  • 每个touch对象有一个全局唯一的identifier。

下面说说这4个东西的设计到底有多坑吧。

坑1

touches,总是记录屏幕上所有的手指,而我关心的是out内的手指操作,怎么判断其中的哪些手指在out区域内呢?

targetTouches,它应该记录了所有在out内手指信息,但是真的是这样吗?

changedTouches:这货记录的是全屏发生变化的手指信息,和touches一样费解。

坑2

我最初在touchevent的各种回调中,希望根据changedTouches追踪out内手指的变化,可惜是做不到的,我来具体说说这个坑。

首先changedTouches记录的是全屏的手指变化,不仅仅是out内的变化,而我原以为只有out内的变化才会通过touchevent回调给我,因此和touches一样难以利用。

其次,changedTouches可能丢失,我认为这是浏览器实现上的问题,在高频率的触摸/离开情况下测试,发现changedTouches偶然会丢失某个手指的离开事件。

也就是说,如果想利用changedTouches来增量的维护手指信息,是一个天坑,你永远填不满。

坑3

后来,我将视线转移到targetTouches,它貌似可以满足我的需求。

于是:每次发生touchevent,我就遍历touchevent.targetTouches内的所有手指,之前没见过的手指就是新加入的,之前存在而现在没有的就是离开的,而剩余的是发生了移动的手指,貌似一切OK。

没想到啊,touchevent的targetTouches并不是这个意思!

我们知道浏览器事件是从内向外冒泡的,虽然我们在out上处理touchevent,但是触摸事件本身其实是在某个in节点上开始的,冒泡到了out。

而每次回调的touchevent,它在外层都有一个touchevent.target,表示本次touchevent是从哪个内层元素冒泡而来的。

我的一根手指按在的第一个in元素上并且挪动,那么touchevent.target就是指向第一个in元素,因此targetTouches也并不是out上的所有手指,而是代表这个target上的所有手指。

可以这样理解:

  • 如果你把2个手指放在2个不同的in元素上,然后彼此挪动手指,那么每次回调的touchevent中,targetTouches只可能包含一根手指,也就是touchevent.target元素上的那根。
  • 如果你把2个手指放在同一个in元素上,然后彼此挪动手指,那么回调的touchevent的targetTouches就有2个了,target是共同的那个in元素。

先知道坑即可,怎么绕着坑走最后再说。

坑4

本以为这样就差不多了,突然又发现了一个天坑。

假设我有一根手指在某个in元素上,然后一段JS代码把这个in元素删了,你猜会发生什么?

即便你把手指离开屏幕,也不会再有任何touchevent回调给out了,就是这样,你的out元素压根不知道这根手指的状态了,没有任何机会。

为了正确的追溯(维护在一个字典中)当前out内的手指状态,我的做法是:在明确知道in元素被删除 或者 你认为in元素可能删除的时候,遍历追溯记录中的所有手指,判断手指所属的target是否仍旧在DOM流中,如果不在了则将手指信息从追溯记录中擦除。

填坑

首先要明确changedTouches是废物,因为会丢,所以不可信赖。

总结一下避坑的原则:

  • 以touches为准,但是需要判定每个touch手指的target是否在out内部,这个可以用$.contains去判断,把属于out的手指挑出来即可,缺点也比较明显:每次touchevent来,都要判断每个target是否在out内属于一个比较浪费性能的DOM操作。
  • 以targetTouches为准,这些touch一定是out内某个target上,但是不包含out内的其他target上的手指,至于怎么办看场景。

 

如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~