Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

fastclick源码解析 #6

Open
Open
Labels
@wython

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); };
}

设计思路

关于设计思路,就是说如果是我们模拟这样一个事件,我们需要怎么去判断为点击事件,我想大概可以从几个点去考虑。

  1. 一个点击事件,它必须包含一个点,一个起的动作(start, end)。
  2. 一个点击事件,它必须是迅速的,也就是说,如果你一直按着不动,那么这不是一个点击事件,而是一个长按事件,所以我们还需要一个时间区间,对于在这个时间区间范围内我们才认为是点击事件。所以我们看fastclick的源码属性中,tapDelay就是这样的作用,默认是200ms,可以人为配置。
  3. 一个点击事件,它必须是短距的,也就是说,如果你移动了一个比较长的距离,那么我们认定为是一个移动事件,这也不算是点击事件,可以在touchMove里面去判断。
  4. 如果满足以上条件,我们就阻止默认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仓库拜读。

其他

最后,补充几个点,

  1. fastclick中,在构造函数阶段,会判断meta标签,如果移动端已经禁止缩放,fastclick不会执行任何操作,以上没有贴出了。
  2. 通过在touchstart和end中,使用preventDefault,会阻止默认的click事件,但是不会影响start --> end的触发。

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

        AltStyle によって変換されたページ (->オリジナル) /