故事:2007 年初。苹果公司在发布首款 iPhone 前夕,遇到一个问题:当时的网站都是为大屏幕设备所设计的。因而苹果的工程师们作了一些约定,应对 iPhone 这种小屏幕浏览桌面端站点的问题。html
这当中最出名的,当属双击缩放(double tap to zoom),这也是会有上述 300 毫秒延迟的主要缘由。git
双击缩放,顾名思义,即用手指在屏幕上快速点击两次,iOS 自带的 Safari 浏览器会将网页缩放至原始比例。 那么这和 300 毫秒延迟有什么联系呢? 假定这么一个场景。用户在 iOS Safari 里边点击了一个连接。因为用户能够进行双击缩放或者双击滚动的操做,当用户一次点击屏幕以后,浏览器并不能马上判断用户是确实要打开这个连接,仍是想要进行双击操做。所以,iOS Safari 就等待 300 毫秒,以判断用户是否再次点击了屏幕。 鉴于iPhone的成功,其余移动浏览器都复制了 iPhone Safari 浏览器的多数约定,包括双击缩放,几乎如今全部的移动端浏览器都有这个功能。以前人们刚刚接触移动端的页面,在欣喜的时候每每不会care这个300ms的延时问题,但是现在touch端界面如雨后春笋,用户对体验的要求也更高,这300ms带来的卡顿慢慢变得让人难以接受。github
也就是说,移动端浏览器会有一些默认的行为,好比双击缩放、双击滚动。这些行为,尤为是双击缩放,主要是为桌面网站在移动端的浏览体验设计的。而在用户对页面进行操做的时候,移动端浏览器会优先判断用户是否要触发默认的行为。浏览器
重点:因为移动端会有双击缩放的这个操做,所以浏览器在click以后要等待300ms,看用户有没有下一次点击,也就是此次操做是否是双击。bash
方案一:禁用缩放
当HTML文档头部包含以下meta
标签时:app
<meta name="viewport" content="user-scalable=no">
<meta name="viewport" content="initial-scale=1,maximum-scale=1">
复制代码
代表这个页面是不可缩放的,那双击缩放的功能就没有意义了,此时浏览器能够禁用默认的双击缩放行为而且去掉300ms的点击延迟。函数
一开始,为了让桌面站点能在移动端浏览器正常显示,移动端浏览器默认的视口宽度并不等于设备浏览器视窗宽度,而是要比设备浏览器视窗宽度大,一般是980px。咱们能够经过如下标签来设置视口宽度为设备宽度。学习
<meta name="viewport" content="width=device-width">
复制代码
由于双击缩放主要是用来改善桌面站点在移动端浏览体验的,而随着响应式设计的普及,不少站点都已经对移动端坐过适配和优化了,这个时候就不须要双击缩放了,若是可以识别出一个网站是响应式的网站,那么移动端浏览器就能够自动禁掉默认的双击缩放行为而且去掉300ms的点击延迟。若是设置了上述meta
标签,那浏览器就能够认为该网站已经对移动端作过了适配和优化,就无需双击缩放操做了。
这个方案相比方案一的好处在于,它没有彻底禁用缩放,而只是禁用了浏览器默认的双击缩放行为,但用户仍然能够经过双指缩放操做来缩放页面。测试
touch-action
这个CSS属性。这个属性指定了相应元素上可以触发的用户代理(也就是浏览器)的默认行为。若是将该属性值设置为touch-action: none
,那么表示在该元素上的操做不会触发用户代理的任何默认行为,就无需进行300ms的延迟判断。
优化
方案一:指针事件的polyfill
如今除了IE,其余大部分浏览器都还不支持指针事件。有一些JS库,可让咱们提早使用指针事件,好比
然而,咱们如今关心的不是指针事件,而是与300ms延迟相关的CSS属性touch-action
。因为除了IE以外的大部分浏览器都不支持这个新的CSS属性,因此这些指针事件的polyfill必须经过某种方式去模拟支持这个属性。一种方案是JS去请求解析全部的样式表,另外一种方案是将touch-action
做为html标签的属性。
方案二:FastClick
FastClick 是 FT Labs 专门为解决移动端浏览器 300 毫秒点击延迟问题所开发的一个轻量级的库。FastClick的实现原理是在检测到touchend事件的时候,会经过DOM自定义事件当即出发模拟一个click事件,并把浏览器在300ms以后的click事件阻止掉。
说完移动端点击300ms延迟的问题,还不得不提一下移动端点击穿透的问题。可能有人会想,既然click点击有300ms的延迟,那对于触摸屏,咱们直接监听touchstart事件不就行了吗?
使用touchstart去代替click事件有两个很差的地方。
第一:touchstart是手指触摸屏幕就触发,有时候用户只是想滑动屏幕,却触发了touchstart事件,这不是咱们想要的结果;
第二:使用touchstart事件在某些场景下可能会出现点击穿透的现象。
什么是点击穿透?
假如页面上有两个元素A和B。B元素在A元素之上。咱们在B元素的touchstart事件上注册了一个回调函数,该回调函数的做用是隐藏B元素。咱们发现,当咱们点击B元素,B元素被隐藏了,随后,A元素触发了click事件。
这是由于在移动端浏览器,事件执行的顺序是touchstart > touchend > click。而click事件有300ms的延迟,当touchstart事件把B元素隐藏以后,隔了300ms,浏览器触发了click事件,可是此时B元素不见了,因此该事件被派发到了A元素身上。若是A元素是一个连接,那此时页面就会意外地跳转。
touchstart --> mouseover(有的浏览器没有实现) --> mousemove(一次) -->mousedown --> mouseup --> click -->touchend
Touch 事件中,经常使用的为 touchstart, touchmove, touchend 三种。除此以外还有touchcancel。 注意,原生事件中并无tap事件。下面会解释tap事件怎么产生的。
事件描述以下:
事件 | 描述 | 触发时机 |
---|---|---|
touchstart | 开始触摸 | 手指接触屏幕时当即触发 |
touchmove | 移动或拖拽 | 取决于系统和浏览器 |
touchend | 触摸结束 | 手指离开屏幕时当即出发 |
而Touch事件的触发通常经过手指,还会存在多点触控,拖拽方向等状况。列出几个重要参数以下:
参数 | 含义 |
---|---|
touches | 屏幕中每根手指信息列表 |
targetTouches | 和touches相似,把同一节点的手指信息过滤掉 |
changedTouches | 响应当前事件的每根手指的信息列表 |
代码获取以下:
elemenrRef.addEventListener('touchstart', function(e) {
console.log(e.touches, e.targetTouches, e.changedTouches);}
);复制代码
手指触发触摸事件的过程以下:
touchstart --> mouseover(有的浏览器没有实现) --> mousemove(一次) -->mousedown -->
mouseup --> click -->touchend
复制代码
由此,咱们能够在 ontouchstart 事件上记录开始触摸开始,ontouchend 记录触摸结束信息。 经过上述这些参数,很容易的去计算幽冥点击的时间,以及点击穿透的相关信息,包括响应的坐标状况。
1) 点击穿透问题:点击蒙层(mask)上的关闭按钮,蒙层消失后发现触发了按钮下面元素的click事件,
蒙层的关闭按钮绑定的是touch事件,而按钮下面元素绑定的是click事件,touch事件触发以后,蒙层消失了,300ms后这个点的click事件fire,event的target天然就是按钮下面的元素,由于按钮跟蒙层一块儿消失了2) 跨页面点击穿透问题:若是按钮下面刚好是一个有href属性的a标签,那么页面就会发生跳转
由于a标签跳转默认是click事件触发,因此原理和上面的彻底相同
3) 另外一种跨页面点击穿透问题:此次没有mask了,直接点击页内按钮跳转至新页,而后发现新页面中对应位置元素的click事件被触发了
和蒙层的道理同样,js控制页面跳转的逻辑若是是绑定在touch事件上的,并且新页面中对应位置的元素绑定的是click事件,并且页面在300ms内完成了跳转,三个条件同时知足,就出现这种状况了
非要细分的话还有第四种,不过几率很低,就是新页面中对应位置元素刚好是a标签,而后就发生连续跳转了。。。诸如此类的,都是点击穿透问题
只用touch
最简单的解决方案,完美解决点击穿透问题
把页面内全部click所有换成touch事件(touchstart
、’touchend’、’tap’),
不用a标签其实没什么,移动app开发不用考虑SEO,即使用了a标签,通常也会去掉全部默认样式,不如直接用span
只用click
不用touch就不会存在touch以后300ms触发click的问题,若是交互性要求不高能够这么作,
tap后延迟350ms再隐藏mask
改动最小,缺点是隐藏mask变慢了,350ms仍是能感受到慢的
只须要针对mask作处理就行,改动很是小,若是要求不高的话,用这个比较省力
pointer-events
比较麻烦且有缺陷,
mask隐藏后,给按钮下面元素添上pointer-events: none;
样式,让click穿过去,350ms后去掉这个样式,恢复响应
缺陷是mask消失后的的350ms内,用户能够看到按钮下面的元素点着没反应,若是用户手速很快的话必定会发现
在下面元素的事件处理器里作检测(配合全局flag)
比较麻烦,
全局flag记录按钮点击的位置(坐标点),在下面元素的事件处理器里判断event的坐标点,若是相同则是那个可恶的click,拒绝响应
上面说的只是想法,没测试过,实在不行就用记录时间戳判断,等待350ms,这样就和pointer-events
差很少
fastclick
好用的解决方案,不介意多加载几KB的话,
别的参考思路(开源库fastclick),取消 click 事件,用touchend 模拟 快速点击行为。
问题来了,click 事件何时触发?
浏览器在 touchend 以后会等待约 300ms ,若是没有 tap 行为,则触发 click 事件。 而浏览器等待约 300ms 的缘由是,判断用户是不是双击(double tap)行为,双击过程当中就不适合触发 click 事件了。 由此能够看出 click 事件触发表明一轮触摸事件的结束。
上面说到原生事件中并无 tap 事件,能够参考经典的 zepto.js 对 singleTap 事件的处理(遗憾的是在部分浏览器中,依然存在点击穿透的问题)。能够看出,singleTap 事件的触发时机 —— 在 touchend 事件响应 250ms 无操做后,触发singleTap。所以,点击穿透的现象就容易理解了,在这 300ms 之内,由于上层元素隐藏或消失了,因为 click 事件的滞后性,一样位置的 DOM 元素触发了 click 事件(若是是 input 则触发了 focus 事件)。在代码中,给咱们的感受就是 target 发生了飘移。
如何处理点击穿透(思路)
1. 触摸开始时 touchstart 事件触发时,preventDefault()。毫无疑问,很容易想到这一点,并且也从根本上解决了这个问题。可是,它有一个避免不了或者说引入了很大的缺陷,页面中DOM 元素没法再进行滚动了。这个方法显然不能知足咱们的需求,可是这个思路其实能够给咱们更多的启发,好比说 iscroll 只容许横向滚动的实现,相关实现这里暂且不表。
2. 触摸结束时 touchend 事件触发时,preventDefault()。看上去好像没有什么问题,可是,很遗憾的是否是全部的浏览器都支持。
3. 禁止页面缩放 经过设置meta标签,能够禁止页面缩放,部分浏览器再也不须要等待 300ms,致使点击穿透。点击事件仍然会触发,但相对较快,因此 click 事件从某种意义上来讲能够取代点击事件, 而代价是牺牲少数用户(click 事件触发仍然较慢)的体验。
<meta name="viewport" content="width=device-width, user-scalable=no">复制代码
移动端chromiun 和 iOS 9.3+ 能够用 CSS 属性来阻止元素的双击缩放进而取消点击穿透的延迟:
html { -ms-touch-action: manipulation; touch-action: manipulation;} 复制代码
4. CSS3 的方法 虽然主要讲的是事件,可是有必要介绍一个 CSS3 的属性 —— pointer-events。
pointer-events: auto | none | visiblePainted | visibleFill | visibleStroke | visible | painted | fill | stroke | all | inherit;复制代码
pointer-events 属性有不少值,有用的主要是 auto 和 none,其余属性为 SVG 服务。
可见移动端开发仍是能够用的。
属性 | 含义 |
---|---|
auto | 默认值,鼠标或触屏事件不会穿透当前层 |
none | 元素再也不是target,监听的元素变成了下层的元素(若是子元素设置成 auto,点击子元素会继续监听事件) |
5. 处理点击事件 —— Touch to Click 最靠谱的方案仍是从点击事件的根源上解决问题。用 js 去判断幽冥点击,而后阻止点击穿透。这种方式显然能够实现,缺点是阻止点击穿透时须要当心,不要致使原生的 HTML 元素(如:连接,多选框,单选框)没法正常运行。
经过上文中介绍的 touches,targetTouches,changedTouches 参数,咱们能够构建出这样的测试页面,能够统计出点击穿透的时间,以及已经响应的状况。
preventDefault() | 点击穿透时间 | 点击穿透区域 | ||||
---|---|---|---|---|---|---|
touchstart | touchend | 缩放页面 | 禁止缩放页面 | 缩放页面 | 禁止缩放页面 | |
Safari Mobile iOS 5.1.1 | Yes | Yes | 370ms after end | 370msafter end | touchstart | touchstart |
Safari Mobile iOS 6.1.3 | Yes | Yes | 370ms after end | 370msafter end | touchstart | touchstart |
Safari Mobile iOS 7.1.1 | Yes | Yes | 370ms after end | 370msafter end | touchstart | touchstart |
Android 2.3.7 | Yes | No | 410ms after end | 410msafter end | touchstart | touchstart |
Android 4.0.4 | Yes | No | 300ms after end | 10ms after end | touchstart | touchstart |
Android 4.1.2 | Yes | No | 300ms after end | 300msafter end | touchstart | touchstart |
Android 4.2.2 | Yes | No | 300ms after start | 10ms after end | touchstart | touchend |
IE10 Windows Phone 8 | No | No | 310ms after end | 10ms after end | touchend | touchend |
Blackberry 10 | Yes | Yes | 260ms after end | 10ms after end | touchstart | touchstart |
Chrome for iOS | Yes | Yes | 360ms after end | 360msafter end | touchstart | touchstart |
Chrome for Android | Yes | Yes | 300ms after start | 10ms after end | touchstart | touchend |
Firefox for Android | Yes | No | 300ms after end | 10ms after end | touchstart | touchend |
由此能够看出: 1. 点击穿透受浏览器和页面是否缩放影响 2. 点击穿透有两种状况:快速状况有 10ms 慢速状况有 300ms 3. 在 touchend 时间上调用 preventDefault() 能够阻止多数状况的点击穿透