FastClick源码分析

FastClick源码分析玩过移动端web开发的同学应该都了解过,移动端上的click事件都会有300毫秒的延迟,这300毫秒主要是浏览器为了判断你当前的点击时单击还是双击,但有时候为了更快的对用户的操作做出更快的响应,越过这个300毫秒的延迟是有点必要的,faskclick做的就是这件事,这篇文章会理清faskclick的整体思路,分析主要的代码,但不会贴出所有的代码,仅分析主干,由于历史原因,faskclick对旧版本…

大家好,又见面了,我是你们的朋友全栈君。

玩过移动端web开发的同学应该都了解过,移动端上的click事件都会有300毫秒的延迟,这300毫秒主要是浏览器为了判断你当前的点击时单击还是双击,但有时候为了更快的对用户的操作做出更快的响应,越过这个300毫秒的延迟是有点必要的,FastClick做的就是这件事,这篇文章会理清FastClick的整体思路,分析主要的代码,但不会贴出所有的代码,仅分析主干,由于历史原因,FastClick对旧版本的机型做了很多兼容性适配,例如ios4,这部分代码到现在显然已经没有什么分析的意义了,所以贴出的代码会将这部分代码删除。

首先,我们分析一下总体的实现思路,其实FastClick做的事情很简单,首先判断当前浏览器需不需要使用FastClick,例如桌面浏览器,那就不需要,直接绕过,接着,如果需要,则在click事件中拦截事件,取消所有绑定事件的操作,接着用一系列touch事件(touchstart,touchmove,touchend)来模拟click事件,由于touch事件不会延迟,从而达到绕过300毫秒延迟的效果。

先看看FastClick是如何判断浏览器是否需要FastClick的

	FastClick.notNeeded = function(layer) {
		var metaViewport;
		var chromeVersion;
		var blackberryVersion;
		var firefoxVersion;

		// Devices that don't support touch don't need FastClick
		//不支持用于模拟的touchstart事件,无法模拟
		if (typeof window.ontouchstart === 'undefined') {
			return true;
		}

		// 探测chome浏览器
		chromeVersion = +(/Chrome\/([0-9]+)/.exec(navigator.userAgent) || [,0])[1];

		if (chromeVersion) {

			//安卓设备
			if (deviceIsAndroid) {
				metaViewport = document.querySelector('meta[name=viewport]');

				if (metaViewport) {
                    // 安卓下,带有 user-scalable="no" 的 meta 标签的 chrome 是会自动禁用 300ms 延迟的,无需 FastClick
					if (metaViewport.content.indexOf('user-scalable=no') !== -1) {
						return true;
					}
					//chome32以上带有 width=device-width的meta标签的也唔需要使用FastClick
					if (chromeVersion > 31 && document.documentElement.scrollWidth <= window.outerWidth) {
						return true;
					}
				}

			// 桌面设备自然无需使用
			} else {
				return true;
			}
		}

		//黑莓浏览器,这个。。。了解就好
		if (deviceIsBlackBerry10) {
			//检测黑莓浏览器
			blackberryVersion = navigator.userAgent.match(/Version\/([0-9]*)\.([0-9]*)/);

			// 黑莓10.3以上部分可以不适用FastClick
			if (blackberryVersion[1] >= 10 && blackberryVersion[2] >= 3) {
				metaViewport = document.querySelector('meta[name=viewport]');

				if (metaViewport) {
					// 跟chome一样
					if (metaViewport.content.indexOf('user-scalable=no') !== -1) {
						return true;
					}
					// 跟chome一样
					if (document.documentElement.scrollWidth <= window.outerWidth) {
						return true;
					}
				}
			}
		}
		
		//ie10带有msTouchAction,touchAction相关样式的不需要FastClick
		if (layer.style.msTouchAction === 'none' || layer.style.touchAction === 'manipulation') {
			return true;
		}

		//firefox,跟chome差不多
		firefoxVersion = +(/Firefox\/([0-9]+)/.exec(navigator.userAgent) || [,0])[1];

		if (firefoxVersion >= 27) {
			// Firefox 27+ does not have tap delay if the content is not zoomable - https://bugzilla.mozilla.org/show_bug.cgi?id=922896

			metaViewport = document.querySelector('meta[name=viewport]');
			if (metaViewport && (metaViewport.content.indexOf('user-scalable=no') !== -1 || document.documentElement.scrollWidth <= window.outerWidth)) {
				return true;
			}
		}
		
		//ie11检测,跟ie10一样,只是ie11废弃了msTouchAction,改为touchAction,依旧是检测样式,检测到相关样式不用FastClick
		if (layer.style.touchAction === 'none' || layer.style.touchAction === 'manipulation') {
			return true;
		}

		//黑名单之外放行,都使用FastClick
		return false;
	};

长长的一大段,基本上采用黑名单策略,分别检测了chome,黑莓,firefox,ie10,ie11,基本上都是检测对应的meta标签,检测到对应的值的话,弃用FastClick,黑名单之外启用FastClick,仅仅是一个检测函数,看看就好,没什么研究的价值

主体流程,看看FastClick的构造函数,此处仅贴出主要代码,删除了一些兼容的代码

	function FastClick(layer, options) {

		//不需要fastClick时直接返回
		if (FastClick.notNeeded(layer)) {
			return;
		}

		//简单的兼容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);
		}
                //捕获阶段做拦截事件处理
		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);


		//处理通过标签属性绑定事件的方式,转化为通过addEventListener绑定事件,确保fastclick的各种兼容能顺利执行
		if (typeof layer.onclick === 'function') {
			oldOnClick = layer.onclick;
			layer.addEventListener('click', function(event) {
				oldOnClick(event);
			}, false);
			layer.onclick = null;
		}
	}

FastClick会在执行FastClick.attach操作时被实例化,从代码我们可以看到,做了几件事,检测是否需要使用FastClick,之后注册了一些列的内部方法(onmouse,onclik,ontouchstart等等)并绑定当前作用域,捕获阶段处理onclick事件,冒泡阶段处理touch相关事件并定义相关的内部处理函数,最后对于用标签绑定事件的方式修改为用addEventListener的方式绑定。至于为什么为什么要在捕获阶段处理onclick,我们都知道,现代浏览器对于事件的处理都是先发生捕获,之后再发生冒泡,而为了兼容旧版本浏览器,默认的做法都是将事件绑定在冒泡阶段,在冒泡阶段处理click事件,我们就可以拦截到click事件,并把后续的click绑定操作全都取消掉。

所以,我们大概可以看到,FastClick里面最主要的几个主要方法:onMouse,onClick,onTouchStart,onTouchMoce,onTouchEnd,onTouchMove,onTouchCancel,接下来我们将会逐个分析这些方法

首先,onClick方法

	FastClick.prototype.onClick = function(event) {
		var permitted;

		// 标记未被取消,直接取消
		if (this.trackingClick) {
			this.targetElement = null;
			this.trackingClick = false;
			return true;
		}

		//submit控件不做处理
		if (event.target.type === 'submit' && event.detail === 0) {
			return true;
		}

		permitted = this.onMouse(event);

		if (!permitted) {
			this.targetElement = null;
		}

		return permitted;
	};

此处有必要解释一下trackingClick和targetElement这两个标记,trackingClick是一个追踪标志,用touch事件模拟时,正常情况下,开始时(touchstart)会被设置为true,模拟结束(touchend)会被设置为false,而click事件会在touchend事件中被模拟发出,这个后面分析代码的时候我们会看到,很明显,这个时候trackingClick如果检测到为true,是一种不正常的现象,这里FastClick的作者解释为you可能使用了类似的第三方库,导致click事件比FastClick更快的发出,所以此处就不再对结果进行处理,并将内部变量重现修改为默认状态。接着,我们看到,onclick方法其实在内部调用了onmouse方法,事实上主要的操作也都是在onmouse里面执行的,接下来我们看看onMouse

	FastClick.prototype.onMouse = function(event) {

	//当前target缺失,有可能模拟触发已经被取消,没有必要阻止 ,直接触发原生事件
	if (!this.targetElement) {
            return true;
        }
        //模拟事件标识符
        if (event.forwardedTouchEvent) {
            return true;
        }

        // 事件无法阻止
        if (!event.cancelable) {
            return true;
        }

		//需要fastclick是阻止所有事件触发,快速点击时亦如此
		if (!this.needsClick(this.targetElement) || this.cancelNextClick) {

			// Prevent any user-added listeners declared on FastClick element from being fired.
			//解除所有后续事件的触发,包括当前节点绑定的其他事件
			if (event.stopImmediatePropagation) {
				event.stopImmediatePropagation();
			} else {

				// Part of the hack for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2)
				event.propagationStopped = true;
			}

			// 阻止冒泡,阻止默认操作
			event.stopPropagation();
			event.preventDefault();
			return false;
		}

		// If the mouse event is permitted, return true for the action to go through.
		return true;
	};

首先,进入onMouse之后,会通过函数needClick判断当前点击的控件是否需要原生点击的支持,避免出现一些bug,然后判断this.cancelNextClick是否为true,cancelNextClick是用于判断当前操作是否要取消的一个标识符,当两次点击的间隔小于配置的值时,cancelNextClick会被设置为true,这个操作在touchend中进行,稍后会进行分析。当条件满足时,执行阻止事件的操作,具体是执行event.stopImmediatePropagation方法,他能阻止此操作之后绑定在这个节点上的所有其他操作,对于不支持的浏览器,会在event中添加一个propagationStopped的属性,用于兼容操作,这个兼容操作后面再说,接着就是各种阻止冒泡,阻止默认操作,至此,整个阻止操作就完成了,接下来就是如何不延迟300毫秒来触发click事件了,上面说了,用touch事件进行模拟,具体如何,往下走

首先,onTouchStart

	FastClick.prototype.onTouchStart = function(event) {
		var targetElement, touch, selection;

		//忽略多点触控
		if (event.targetTouches.length > 1) {
			return true;
		}

		targetElement = this.getTargetElementFromEventTarget(event.target);
		touch = event.targetTouches[0];

		//记录跟踪状态
		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;
	};

onTouchStart做的事情其实比较少,上面的代码去掉了一些兼容性操作,剩下的只是记录一些基础性的信息,唯一做的事情就是阻止了双击事件的默认操作,如何判断是双击的,event.timeStamp记录了当前点击的时间戳,this.lastClickTime为上一次onTouchEnd时记录的值,记录最后一次点击完成的时间,两者相减小于配置值,则认为是双击,FastClick默认配置的this.tapDelay为200毫秒

接着是onTouchMove

	FastClick.prototype.onTouchMove = function(event) {
		//没有触发过touchstart事件,直接返回
		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;
	};

操作也是比较简单,trackingClick是一个跟踪字段,在onTouchStart中设置为true,如此处发现不为true,则发生了错误,直接会返回,接着就是判断当前是否有移动,主要就是获取当前手指的位置跟触发控件的位置进行比较,具体方法由于篇幅关系就不解释了,本篇博文仅解释主干内容,当触摸点移动了,则将trackingClcik和targetElement恢复为默认,之后在touchEnd中就不会发出模拟事件触发click

接着对于特殊原因取消的情况,绑定了touchcancel事件

	FastClick.prototype.onTouchCancel = function() {
		this.trackingClick = false;
		this.targetElement = null;
	};

这个并没有什么特别的地方,特殊情况发生了,如手指戳下的时候突然来电话了各种情况导致触摸中断,则将所有跟踪变量恢复到初始状态。

最关键的onTouchEnd

	FastClick.prototype.onTouchEnd = function(event) {
		var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement;
                //触摸点移动或者其他操作导致取消
		if (!this.trackingClick) {
			return true;
		}

		//不处理快速点击
		if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {
			this.cancelNextClick = true;
			return true;
		}

		//不处理长按
		if ((event.timeStamp - this.trackingClickStart) > this.tapTimeout) {
			return true;
		}

		// 将所有的跟踪变量设置为初始状态,供下次点击使用
		this.cancelNextClick = false;

		this.lastClickTime = event.timeStamp;

		trackingClickStart = this.trackingClickStart;
		this.trackingClick = false;
		this.trackingClickStart = 0;

		targetTagName = targetElement.tagName.toLowerCase();
		//处理组件为label时的状况,获取label对应绑定的控件
		if (targetTagName === 'label') {
			forElement = this.findControl(targetElement);
			if (forElement) {
				this.focus(targetElement);
				if (deviceIsAndroid) {
					return false;
				}

				targetElement = forElement;
			}
		} else if (this.needsFocus(targetElement)) {

		//第一个判断作者认为如果按下的时间超过了100毫秒,此时已经没有必要再执行模拟操作了,按原生的click执行操作即可,第二个判断则是处理ios相关的一个bug
			if ((event.timeStamp - trackingClickStart) > 100 || (deviceIsIOS && window.top !== window && targetTagName === 'input')) {
				this.targetElement = null;
				return false;
			}

			this.focus(targetElement);
			this.sendClick(targetElement, event);

			return false;
		}

		//不需要原生点击时,触发模拟click事件
		if (!this.needsClick(targetElement)) {
			event.preventDefault();
			this.sendClick(targetElement, event);
		}

		return false;
	};

此处,ontouchEnd,首先忽略快速点击和长按,然后恢复所有的初始化变量,之后会判断当前控件是不是label,是的话利用findControl函数找到label关联的组件,并赋值给当前的targetElement 统一处理,具体杂七杂八的函数会在后面再解释,接着会判断当前组件触发click时需不需要获取焦点,如果需要,则获取焦点后,触发模拟事件,此处关注两个函数focus和sendClick,focus函数帮助当前target获取焦点,sendClick则发送模拟事件,focus函数关键代码如下

	/**
	 * 兼容写法,获取焦点,光标放置到末尾
	 */
	FastClick.prototype.focus = function(targetElement) {
		var length;
		if (deviceIsIOS && targetElement.setSelectionRange && targetElement.type.indexOf('date') !== 0 && targetElement.type !== 'time' && targetElement.type !== 'month') {
			length = targetElement.value.length;
			targetElement.setSelectionRange(length, length);
		} else {
			targetElement.focus();
		}
	};

此处,对于ios浏览器,采用兼容的写法,用setSelectionRange来获取焦点,setSelectionRange可以用来选取输入框的值,此处将选取的开始和结束都设置为value的length,则可以把光标放到组件的末尾并且获得焦点

接下来是sendClick,这也是整个fastclick的关键,用于模拟事件的发生,主要实现如下:

	FastClick.prototype.sendClick = function(targetElement, event) {
		var clickEvent, touch;

		//兼容操作,部分安卓机当前焦点所在的节点如果不是模拟节点,需要把焦点去除,否则影响效果
		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);
	};
实现代码很简单,就是就是创建一个event对象,然后触发它,注意,这个地方用到了initMouseEvent来初始化event对象,但目前initMouseEvent已经从web删除了,换句话说它已经不是标准方法了,未来的浏览器可能不会再继续提供支持,所以自己尽量不要使用这个特性,可以用MouseEvent这个特定的事件构造器来替代它,详细使用方法可以参考
戳我带你飞

至此,我们的所有主流程已经讲完了,接下来我们说一下里面涉及到的一些杂七杂八的函数

首先,如何兼容event.stopImmediatePropagation,上面我们说了,这个函数可以解除当前绑定操作之后的所有绑定到此节点上的操作,但存在部分浏览器不兼容,对于一些不兼容的浏览器,上面说到绑定事件fastclick会手动给event对象添加一个propagationStopped属性,那这个属性有什么用呢,我们看看下面的代码

layer.addEventListener = function(type, callback, capture) {
				var adv = Node.prototype.addEventListener;
				if (type === 'click') {
					adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) {
						//通过对event对象添加属性来控制事件的触发
						if (!event.propagationStopped) {
							callback(event);
						}
					}), capture);
				} else {
					adv.call(layer, type, callback, capture);
				}
			};

这段函数出现在fastclick的构造函数中,为了主干代码的清晰,在上面我把它删掉了,对于不兼容event.stopImmediatePropagation的浏览器,它重写了addEventListener方法,增加了对stopImmediatePropagation属性的判断,这样当上面的propagationStopped被设置为true的时候,后续的绑定操作就都不会继续进行了。

接下来一个方法是获取label关联控件的方法,findControl

	FastClick.prototype.findControl = function(labelElement) {

		//通过control属性获取
		if (labelElement.control !== undefined) {
			return labelElement.control;
		}

		//通过获取for属性
		if (labelElement.htmlFor) {
			return document.getElementById(labelElement.htmlFor);
		}

		//如各种不兼容,则获取label标签中的第一个
		return labelElement.querySelector('button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea');
	};

首先,findControl会通过html5的control属性来获取label包含的表单元素,如果失败,转而获取label的for属性对应的表单元素,因为for属性也是html5的,旧浏览器可能不兼容,最后如果获取不了,则会获取label元素的子元素中的第一个表单元素,进而来获取label对应的表单元素。

嗯,啰啰嗦嗦大概说完了,如有说错的地方,欢迎评论区指出

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/150808.html原文链接:https://javaforall.cn

【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛

【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...

(0)


相关推荐

发表回复

您的电子邮箱地址不会被公开。

关注全栈程序员社区公众号