抛出几个问题

FastClick是什么?

FastClick是一个简单、易用的库,它可以消除手机浏览器上tap和正在触发的click事件之间的延迟,这个延迟时间大约是300ms。

目的是什么呢?

目的是让提升应用的响应速度,同时,避免任何对当前逻辑的干扰。想一想如果我们每次点击屏幕的时候,都要在300ms之后才能看到响应,是多么尴尬的事情,会让人觉得卡顿。

为什么会有延迟呢?

简单来说,移动浏览器都支持双击缩放或者双击滚动的操作,由于用户第一次点击屏幕后,浏览器不能马上判断用户是要打开链接,还是想要双击,为此Safari在点击事件上加了300ms的延迟,用以判断用户点击屏幕的真正意图,后来几乎所有浏览器都效仿了这个做法。

如何避免300ms的延迟呢?

Zepto的tap事件是一个办法,但会有点透的问题,但是最新版的Zepto已经修复了这个问题。在Zepto修复问题之前,fastclick、hammer等通用库可以使用。说到Zepto之前存在的点透问题,在这里进行简单的说明。

什么是点透?

点透就是点击穿透的意思,感觉这样解释好像很简单易懂的样子,举个栗子来说明一下吧:
设定一个场景,有两个重叠的元素A和B,其中A叠在B的上方,B元素内有个Button按钮,当点击A的时候,将A隐藏,但这个时候却意外的触发了Button的点击事件,即明明点击了A,却穿透A点击到了B元素内的Button按钮,这就是所谓的点透。

为什么点透?

这就跟那300ms的延迟有关了。在移动端,当用触摸屏幕的时候不使用click而实用touch(touchstart、touchend)是因为click会有明显的延迟,造成卡顿。那么click和touch事件有什么区别呢?如下:

  • touchstart:当手指触摸DOM(或者冒泡到该DOM)时,将立即触发;
  • click:当手指触摸屏幕后,浏览器需要大约300ms的延迟,来判断是不是单纯的click事件。
    也就是说,事件的触发会按照touchstart、touchend、click的顺序执行,也就是说touchstart阶段就已经隐藏了A元素,当click被触发的时候,能够被点击的元素是B元素内的Button按钮,因此产生了点透问题。

Zepto的tap事件

上面已经说了,为了代替click事件(也就是为了避免300ms的延迟),Zepto提出了touch.js插件中的tap事件,但tap存在点透现象:

  • 同页面tap点击弹出弹层,弹层中也有一个button,正好重叠的时候,会出现击穿
  • tap事件点击,页面跳转,新页面中同位置也有一个按钮,会出现击穿

通过Zepto源码,先来看看Zepto的tap点透问题是怎么产生的,源码中Zepto对 singleTap 事件的处理是这样儿的:

1
2
3
4
5
6
7
8
//trigger single tap after 250ms of inactivity
else {
touchTimeout = setTimeout(function(){
touchTimeout = null
if (touch.el) touch.el.trigger('singleTap')
touch = {}
}, 250)
}

可以看出在touchend响应250ms无操作后,就会触发singleTap,Zepto中的tap通过兼听绑定在document上的touch事件来完成tap事件的模拟的,是通过事件冒泡实现的。在点击完成时(touchstart/touchend)的tap事件需要冒泡到document上才会触发。而在冒泡到 document 之前,手指接触和离开屏幕(touchstart/touchend)是会触发click事件的。

还是来看看fastclick是怎么办到的吧

解释完Zepto的点透问题后,我们还是回到fastclick的解决方案中来,关于其他库我在这暂且不说了,想要知道fastclick是如何搞定300ms延迟的当然要打开它的源码一探究竟了,Open it!!!!

先看到fastclick的入口:

1
2
3
4
5
6
7
8
9
/**
* Factory method for creating a FastClick object
*
* @param {Element} layer The layer to listen on
* @param {Object} [options={}] The options to override the defaults
*/
FastClick.attach = function(layer, options) {
return new FastClick(layer, options);
};

可以看到,入口方法包含两个参数:

  • layer:要监听的DOM对象,doucument.body
  • options:自定义参数

在入口下面是一段兼容代码:

1
2
3
4
5
6
7
8
9
10
11
12
if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) {
// AMD. Register as an anonymous module.
define(function() {
return FastClick;
});
} else if (typeof module !== 'undefined' && module.exports) {
module.exports = FastClick.attach;
module.exports.FastClick = FastClick;
} else {
window.FastClick = FastClick;
}

作为一个插件,这段代码使fastclick兼容了AMD、commonJS以及原生JS。

沿着入口,下面我们需要关注的就是fastclick的构造函数了。
首先看到一堆属性,其中需要重点关注这仨:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* Touchmove boundary, beyond which a click will be cancelled.
*
* @type number
*/
this.touchBoundary = options.touchBoundary || 10;
/**
* The minimum time between tap(touchstart and touchend) events
*
* @type number
*/
this.tapDelay = options.tapDelay || 200;
/**
* The maximum time for a tap
*
* @type number
*/
this.tapTimeout = options.tapTimeout || 700;

继续往下看,我们看到一个if语句,用来判断是否需要调用fastclick,在notNeeded方法中对多种情况进行了过滤,具体哪些情况可以对照着when-it-isnt-needed来看。

1
2
3
if (FastClick.notNeeded(layer)) {
return;
}

然后是将自定义方法绑定到对应的事件上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Some old versions of Android don't have Function.prototype.bind
function bind(method, context) {
return function() { return method.apply(context, arguments); };
}
var methods = ['onMouse', 'onClick', 'onTouchStart', 'onTouchMove', 'onTouchEnd', 'onTouchCancel'];
var context = this;
for (var i = 0, l = methods.length; i < l; i++) {
context[methods[i]] = bind(context[methods[i]], context);
}
// Set up event handlers as required
if (deviceIsAndroid) {
layer.addEventListener('mouseover', this.onMouse, true);
layer.addEventListener('mousedown', this.onMouse, true);
layer.addEventListener('mouseup', this.onMouse, true);
}
layer.addEventListener('click', this.onClick, true);
layer.addEventListener('touchstart', this.onTouchStart, false);
layer.addEventListener('touchmove', this.onTouchMove, false);
layer.addEventListener('touchend', this.onTouchEnd, false);
layer.addEventListener('touchcancel', this.onTouchCancel, false);

下面是对旧版本Android不支持stopImmediatePropagation 事件的兼容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Hack is required for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2)
// which is how FastClick normally stops click events bubbling to callbacks registered on the FastClick
// layer when they are cancelled.
if (!Event.prototype.stopImmediatePropagation) {
layer.removeEventListener = function(type, callback, capture) {
var rmv = Node.prototype.removeEventListener;
if (type === 'click') {
rmv.call(layer, type, callback.hijacked || callback, capture);
} else {
rmv.call(layer, type, callback, capture);
}
};
layer.addEventListener = function(type, callback, capture) {
var adv = Node.prototype.addEventListener;
if (type === 'click') {
adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) {
if (!event.propagationStopped) {
callback(event);
}
}), capture);
} else {
adv.call(layer, type, callback, capture);
}
};
}

当然,还需要兼容直接绑定到DOM上的onclick事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
// If a handler is already declared in the element's onclick attribute, it will be fired before
// FastClick's onClick handler. Fix this by pulling out the user-defined handler function and
// adding it as listener.
if (typeof layer.onclick === 'function') {
// Android browser on at least 3.2 requires a new reference to the function in layer.onclick
// - the old one won't work if passed to addEventListener directly.
oldOnClick = layer.onclick;
layer.addEventListener('click', function(event) {
oldOnClick(event);
}, false);
layer.onclick = null;
}

接下来,又是一系列的兼容,需要说明的是:

  • touchHasMoved 手指点击时移动间距大于10px,返回true
  • onTouchMove 手指点击时移动间距大于10px,即视为touchmove,不触发模拟click事件

然后,是一些特殊情况的处理:

  • needsClick 确定哪些元素需要原生的click事件
  • needsFocus 确定哪些元素需要原生的focus事件
    大概意思就是如果需要使用原生click或者focus事件,需要给DOM添加class=’needsClick’

再往下,就是fastclick的核心了!!!

  • onTouchStart
  • onTouchEnd
  • sendClick
    这里,我们抽出主要代码:
1
2
3
4
5
6
7
//###########onTouchStart###############
FastClick.prototype.onTouchStart = function(event) {
//tapDelay默认300毫秒,点击时间差小于300毫秒,则阻止事件再次触发,阻止短时间内双击的问题
if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {
event.preventDefault();
}
}
1
2
3
4
5
6
7
//###########onTouchEnd###############
if (!this.needsClick(targetElement)) {
// 如果这不是一个需要使用原生click的元素,则屏蔽原生事件,避免触发两次click
event.preventDefault();
// 触发一次模拟的click
this.sendClick(targetElement, event);
}
1
2
3
4
5
6
7
8
9
10
11
//###########sendClick###############
//这个事件会在onTouchEnd中用到,经过一系列的判断,符合条件,调用这个模拟事件
FastClick.prototype.sendClick = function(targetElement, event) {
var clickEvent, touch;
//创建一个鼠标事件
clickEvent = document.createEvent('MouseEvents');
//初始化鼠标事件
clickEvent.initMouseEvent(this.determineEventType(targetElement), true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);
//触发这个事件
targetElement.dispatchEvent(clickEvent);
};

参考

300ms的起源
fastclick源码解读
彻底解决tap“点透”,提升移动端点击响应速度
点透问题