历史
之所以移动端网页在点击的时候的时候会出现 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 |
第二个证明
在同一个元素上同时绑定 touchend
和 click
事件,通过 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-action
(W3C 链接)的鼻祖其实是微软发明出来 pointer-event
。touch-action
实际上在这种情况下只会用一个值 none
,规定这个元素不对任何用户 touch 操作进行任何反馈。
这里也需要注意,比方说,一个元素绑定了 touch 事件的监听,在 CSS 规定了 touch-action
为 none
,那么点击是不会触发事件的。但是如果使用 JavaScript 将 touch-action
的值修改为其它,比如 auto
,事件又会被执行,再改回来之后就会失效。
FastClick 等第三方库
FastClick 和 Zepto 我本身用的比较多。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.
参考
- What Exactly Is….. The 300ms Click Delay
- The 300 ms Click Delay and iOS 8
- Introduction to Touch events in JavaScript
- Detect left/right-swipe on touch-devices, but allow up/down-scrolling
技术发展迭代很快,所以这些笔记内容也有类似新闻的时效性,不免有过时、或者错误的地方,欢迎指正 ^_^。
BEST
Lien(A.K.A 胡椒)