-
Notifications
You must be signed in to change notification settings - Fork 0
Description
fastclick是什么,它解决了什么问题呢?fastclick解决了移动端点击的约300ms延迟问题。当触发click事件时,无法判断用户是想进行双击还是单击,所以有个约300ms的判断是否会进行第二次点击操作。
基本结构
function FastClick(layer, options) {}
// ......
FastClick.attach = function(layer, options) {
return new FastClick(layer, options);
};
// 这个方法用于判断是否需要fastclick,因为如果用户设置了如禁止播放的功能,
// 那么默认将不需要fastClick, 这个方法代码长,这里不贴,里面是关于不同版本
// 浏览器判断和对user-scalable的获取判断
FastClick.notNeeded = function(layer) { // ... }可以看到基本结构很简单,定义一个构造函数,并且挂载一个静态函数attach(js没有面向对象语法,但是如果在构造函数挂载函数,实际上等同于面向对象的Class.staticMethod,即不需要实例即可访问的函数,同理,原型上的方法是需要创建实例才能使用)。
attach方法就是创建一个FastClick实例,所以基本上,fastClick的操作就在构造函数初始化内完成。接下来继续看里面的内容。
对象结构
attach方法已经拜读,可以先忽略了。
// 对象属性
function FastClick(layer, options) {
this.trackingClick = false; // 当touchStart时候置为true
this.trackingClickStart = 0; // 当touchStart时候的时间错
this.targetElement = null; // 触发的元素
this.touchStartX = 0; // touch位置x轴
this.touchStartY = 0; // touch位置y轴
this.lastTouchIdentifier = 0; // 注释:最后触摸的id,取自Touch.identifier.
this.touchBoundary = options.touchBoundary || 10; // move边界
this.layer = layer; // 挂载的dom元素,该dom元素下fastclick将生效
this.tapDelay = options.tapDelay || 200;
this.tapTimeout = options.tapTimeout || 700;
// ... 执行过程
}构造函数执行过程
function FastClick(layer, options) {
// ...属性已省略
var methods = ['onMouse', 'onClick', 'onTouchStart', 'onTouchMove', 'onTouchEnd', 'onTouchCancel']; // 列举需要挂载实例中回到到layer上的事件
var context = this;
for (var i = 0, l = methods.length; i < l; i++) {
context[methods[i]] = bind(context[methods[i]], context); // 绑定fastclick对象的上下文给回调函数, 这些回调是定义在构造函数的prototype上,后面会看到
}
// 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);
// ...
}构造函数核心过程如图,它会给layer挂载需要处理的回调事件,layer作为上层节点,能够获取到target节点所发生的事件,至于在各种事件中如何处理这个问题,我们会在后续的代码中看到。
bind方法的实现如下, 简单看下,没啥说的,比较简单:
// Some old versions of Android don't have Function.prototype.bind
function bind(method, context) {
return function() { return method.apply(context, arguments); };
}设计思路
关于设计思路,就是说如果是我们模拟这样一个事件,我们需要怎么去判断为点击事件,我想大概可以从几个点去考虑。
- 一个点击事件,它必须包含一个点,一个起的动作(start, end)。
- 一个点击事件,它必须是迅速的,也就是说,如果你一直按着不动,那么这不是一个点击事件,而是一个长按事件,所以我们还需要一个时间区间,对于在这个时间区间范围内我们才认为是点击事件。所以我们看fastclick的源码属性中,tapDelay就是这样的作用,默认是200ms,可以人为配置。
- 一个点击事件,它必须是短距的,也就是说,如果你移动了一个比较长的距离,那么我们认定为是一个移动事件,这也不算是点击事件,可以在touchMove里面去判断。
- 如果满足以上条件,我们就阻止默认click事件,并且自定义一个事件click
Fastclick的原型方法
原型方法即实例方法
FastClick.prototype.focus = function(targetEle) { // ... };
FastClick.prototype.updateScrollParent = function(targetEle) { // .. };
FastClick.prototype.getTargetElementFromEventTarget = function(eventTarget) { // ... };
FastClick.prototype.touchHasMoved = function(event) { // ... };
FastClick.prototype.findControl = function(event) { // ... };
FastClick.prototype.onTouch = function(event) { // ... };
FastClick.prototype.destroy = function(event) { // ... };
FastClick.prototype.onTouchStart = function(event) { // ... };
FastClick.prototype.onTouchMove = function(event) { // ... };
FastClick.prototype.onMouse = function(event) { // ... };
FastClick.prototype.onClick = function(event) { // ... };
FastClick.prototype.onTouchCancel = function(event) { // ... };首先,按照我们预想的思路, 应该想看touchStart方法.
FastClick.prototype.onTouchStart = function(event) {
var targetElement, touch, selection;
// Ignore multiple touches, otherwise pinch-to-zoom is prevented if both fingers are on the FastClick element (issue #111).
if (event.targetTouches.length > 1) {
return true;
}
targetElement = this.getTargetElementFromEventTarget(event.target);
touch = event.targetTouches[0];
if (deviceIsIOS) { // ... 此省略部分issue兼容代码 }
this.trackingClick = true;
this.trackingClickStart = event.timeStamp;
this.targetElement = targetElement;
this.touchStartX = touch.pageX;
this.touchStartY = touch.pageY;
// Prevent phantom clicks on fast double-tap (issue #36)
if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {
event.preventDefault();
}
return true;
}touchStart方法主要存储几个状态属性, trackingClick用于其他事件里面追逐,是否触发click以这个属性为准,trackingClickStart 用于存储点下去时候的时间戳, touchStartX, touchStartY分别为点击事件的X, Y轴。至于其他工作,比如忽略多指操作(即不追踪)。
FastClick.prototype.onTouchHove = function(event) {
// trackingClick是是否追踪,不追踪直接返回
if (!this.trackingClick) {
return true;
}
// If the touch has moved, cancel the click tracking
if (this.targetElement !== this.getTargetElementFromEventTarget(event.target) || this.touchHasMoved(event)) {
this.trackingClick = false;
this.targetElement = null;
}
return true;
}这是touchMove的处理事件,主要就是两个判断,一个是如果move事件触发元素和touchStart的元素不是一个元素,即已经有移动,则不触发,touchHasMoved方法必然是用于算移动距离是否已经超过限制,超过则不追踪click。
touchHasMoved实现如下:
FastClick.prototype.touchHasMoved = function(event) {
var touch = event.changedTouches[0], boundary = this.touchBoundary;
if (Math.abs(touch.pageX - this.touchStartX) > boundary || Math.abs(touch.pageY - this.touchStartY) > boundary) {
return true;
}
return false;
};这里有个值得了解的e.changedTouches属性, 属于touch event属性,学习了。
接下来就是最后一步,touchEnd,按我们预计,最后应该是自定义click事件。我看了下,整个end事件处理很多东西,但是最终做了这些事情,判断是否需要click,(包括长按,之前move,start不满足click条件的都return ture), 然后阻止默认click事件,自定义click事件,fastClick会对class为needsClick的元素进行过滤,即不需要fastclick的元素将不会执行自定义click事件。关于其兼容判断,我暂时删除。删除完的代码如下:
FastClick.prototype.onTouchEnd = function (event) {
if (!this.trackingClick) {
return true;
}
// 长按校验
if ((event.timeStamp - this.trackingClickStart) > this.tapTimeout) {
return true;
}
// 阻止默认click事件, 自定义click事件
if (!this.needsClick(event.target)) {
event.preventDefault();
this.sendClick(targetElement, event);
}
}到这里流程其实已经结束了,最后我们还可以看下如何去模拟click实现,关于sendClick,fastClick是这样实现的:
FastClick.prototype.sendClick = function(targetElement, event) {
var clickEvent, touch;
// On some Android devices activeElement needs to be blurred otherwise the synthetic click will have no effect (#24)
if (document.activeElement && document.activeElement !== targetElement) {
document.activeElement.blur();
}
touch = event.changedTouches[0];
// Synthesise a click event, with an extra attribute so it can be tracked
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);
clickEvent.forwardedTouchEvent = true;
targetElement.dispatchEvent(clickEvent);
};
FastClick.prototype.determineEventType = function(targetElement) {
//Issue #159: Android Chrome Select Box does not open with a synthetic click event
if (deviceIsAndroid && targetElement.tagName.toLowerCase() === 'select') {
return 'mousedown';
}
return 'click';
}; 实际上,就是通过DOM接口的createEvent创建一个模拟事件,DOM提供了模拟事件的api: createEvent(TYPE), 此时会返回一个initMouseEvent方法,需要通过这个方法定义相关的事件名称,和一些额外的参数,最后需要dispatchEvent这个事件。自定义事件的流程为:
createEvent ---> initMounseEvent ---> dispatchEvent
叫做click覆盖默认的click,这个事件依然会冒泡,依然可以委托。这就是整个fastclick的思路,到这里,就结束了。但是fastclick还有很多兼容性工作,感兴趣的可以移步到fastclick仓库拜读。
其他
最后,补充几个点,
- fastclick中,在构造函数阶段,会判断meta标签,如果移动端已经禁止缩放,fastclick不会执行任何操作,以上没有贴出了。
- 通过在touchstart和end中,使用preventDefault,会阻止默认的click事件,但是不会影响start --> end的触发。