笔记:移动端 touch 事件(一)300ms 点击延迟的证明、「点穿」和解决方案


历史

之所以移动端网页在点击的时候的时候会出现 300ms 延迟,其实有一定的历史原因。具体的源头,可能就要说到另移动端(或者说移动互联)兴起的 iPhone。

最早,在第一代 iPhone 发布的之前,Apple 的研发团队面另一个问题,如何在 iPhone 那么小的屏幕上让用户没有阻碍地访问、阅读平时在电脑前浏览的网页,毕竟电脑显示器的尺寸是手机显示器尺寸的很多倍。

Apple 解决的方式也很简单、自然,提供一个经典的「手势」:双击放大(Double Tap to Zoom),让用户可以通过很简单的点击操作就可以放大网页上的某一部分,进行浏览。

试着以程序的角度理解一下:双击的前提是要有第一次单击,第一次单击之后会监听第二次连续的单击,两次连续的单击被称为一个双击。问题就在于,这个「连续」,以多少时间间隔为限制让程序认定这是个一个连续的双击,而不是两次分开的单击。在这里,当年 Apple 用了 300ms 为阈值,来判定:在 300ms 以内的两次连续单击是一个双击事件,所以才有了后来点击事件的各种噩梦一样的兼容问题。

较简单的例子

比如说:页面上有一个链接,用户在点击了这个链接之后,浏览器不会立即做回馈,因为它不知道用户确实是点击链接,还是要执行一个双击操作,浏览器会等待 300ms 之后,认定这是一个单击,而不是双击,才会执行点击链接的默认操作–进行页面的跳转。

视频

这篇文章 What Exactly Is….. The 300ms Click Delay 中的两个视屏,有 300ms 延迟和没有的直观表现:

有 300ms 延迟:
没有 300ms 延迟:

移动端各种事件触发顺序:

笔记的来由,是为了整理移动端的点击延迟问题、和其造成点击穿透问题的证明,第一点要明确的是移动端各种事件出发顺序是:

> touchstart > touchmove > touchend > mousedown
> mousemove > mouseenter > click > 结束。

这位开发者在制作了一个各种硬件上不同浏览器对 touch 和 click 的测试的页面:测试的结果汇总;相比以下我在 CodePen 制作的简单测试,他还提供了非常详尽功能测试的页面:针对 event listener,和相关的索引

click 延迟的两个证明:

两个 CodePen,都需要在移动设备上访问,桌面端没有做对于无法获取 touchend 事件的兼容,会造成结果输出为 NaN

第一个是将上面提到的的顺序依次打印:

See the Pen YQVLJd by Lien (@movii) on CodePen.

一组 sample 数据:

event timestamp
start time 0
touchmove -
touchend 47
mousedown 437
mouseover 434
mouseenter 471
click 440
click delay 392

第二个证明

在同一个元素上同时绑定 touchendclick 事件,通过 Date.now() 将两者的触发时间相减获得具体的延迟时间,需要在移动端设备上访问。

See the Pen ZyKoVK by Lien (@movii) on CodePen.

移动端点击事件的「点穿」

基于以上的点击时间的延迟,会导致一个「点穿」的问题,也被叫做 「ghost click」。原因可以描述为:上下两个 div 重叠,下方的 div 绑定了 click,上方的 div 绑定了 touch 中的一个事件监听,且 touch 后,上方的元素会隐藏的话,就会造成穿透,因为 click 是在 touch 之后延迟触发,浏览器会误认为是在遮盖的元素上触发了 click。

点穿的证明

同样也做了 demo。原理是上下两个层重叠,对两者都加上计数功能-记录点击事件。不同的是上层的 div 绑定在 touchend 事件,下层的绑定在 click 事件。而上层的 div 在接收到 touchend 事件之后会立即 display: none;,之后下方的 div 的 click 计数就会增加(需要在移动端设备进行访问)。

See the Pen click delay - ghost click by Lien (@movii) on CodePen.

解决方案

通过 meta 标签禁用「双击放大」

上面说到,既然是因为有「双击放大(Double Tap to Zoom)」这个操作导致浏览器需要预留出时间来检测是否是双击事件,从而导致 touch 事件有 300ms 的延迟。那么第一个解决方式就和简单,全局禁用「双击放大」,在页面里添加 meta。

这么做虽然可以解决 300ms 点击延迟的问题,可同时也带来了很大的负面问题:用户完全不能放大页面了。想象一下那些整个页面都是密密麻麻文字,完全没有针对移动端设备优化的网页,让用户怎么看?

所以很快,Chrome 发布了新的优化,同样也是 <meta name="viewport">,但是具体的值修改成了 content="width=device width"。只要页面添加了这个 meta值小于或等于 device-width,浏览器就会禁用「双击放大」

这里有个前提概念,一般页面在没有任何 viewport 设置的时候,都跟随 iPhone 最早发布时候的做法,对每个页面都是初始化为 980px 的宽度,而不是设备自己本身的宽度,比如说最早 iPhone 自己的 320px。

同时,有了这个 viewport,除了禁用了「双击放大」的操作,实际上还告诉了浏览器,现在访问的这个页面是针对移动端优化过显示的。再者,虽然「双击放大」被禁用,使用这个 meta 却可以保证双指 pinch 的放大操作仍然奏效。

CSS 中的 touch-action

touch-actionW3C 链接)的鼻祖其实是微软发明出来 pointer-eventtouch-action 实际上在这种情况下只会用一个值 none,规定这个元素不对任何用户 touch 操作进行任何反馈。

这里也需要注意,比方说,一个元素绑定了 touch 事件的监听,在 CSS 规定了 touch-actionnone,那么点击是不会触发事件的。但是如果使用 JavaScript 将 touch-action 的值修改为其它,比如 auto,事件又会被执行,再改回来之后就会失效。

FastClick 等第三方库

FastClickZepto 我本身用的比较多。FastClick 的做法是,在每一次 touchend 发生时,都触发一个 DOM Custom Event,然后把 300ms 之后浏览器触发的原生 Click Event 阻止。关于两者的解读有很多,比如这里

关于 FastClick 可以用以下这段 snippet 来理解:

demo

这个 demo 使用 👆 这段实现来获得 tap 事件(没有针对桌面做 touch 的 mapping,需要在移动端或者 Dev Tools 的simulator 中打开)。

See the Pen 38119963ad81c67a557c5689a5624cec by Lien (@movii) on CodePen.

参考

感谢阅读

你们好, 2018 年初把小站从 Jekyll 迁移到 Hugo 的过程中,删除了评论区放的 Disqus 插件,考虑有二:首先无论评论、还是对笔记内容的进一步讨论,读者们更喜欢通过邮件、或者 Twitter 私信的方式来沟通;其次一年多以来 Disqus 后台能看到几乎都是垃圾留言(spam),所以这里直接贴一下邮件、以及 Twitter 账户 地址。

技术发展迭代很快,所以这些笔记内容也有类似新闻的时效性,不免有过时、或者错误的地方,欢迎指正 ^_^。

BEST
Lien(A.K.A 胡椒)
本站总访问量 本站总访客量 本文总阅读量