欢迎访问 生活随笔!

生活随笔

当前位置: 首页 > 编程资源 > 编程问答 >内容正文

编程问答

FastClick源码分析

发布时间:2024/1/1 编程问答 41 豆豆
生活随笔 收集整理的这篇文章主要介绍了 FastClick源码分析 小编觉得挺不错的,现在分享给大家,帮大家做个参考.

玩过移动端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 延迟的,无需 FastClickif (metaViewport.content.indexOf('user-scalable=no') !== -1) {return true;}//chome32以上带有 width=device-width的meta标签的也唔需要使用FastClickif (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以上部分可以不适用FastClickif (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相关样式的不需要FastClickif (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=922896metaViewport = 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,依旧是检测样式,检测到相关样式不用FastClickif (layer.style.touchAction === 'none' || layer.style.touchAction === 'manipulation') {return true;}//黑名单之外放行,都使用FastClickreturn 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相关的一个bugif ((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 trackedclickEvent = 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对应的表单元素。

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

总结

以上是生活随笔为你收集整理的FastClick源码分析的全部内容,希望文章能够帮你解决所遇到的问题。

如果觉得生活随笔网站内容还不错,欢迎将生活随笔推荐给好友。