大家好,又见面了,我是你们的朋友全栈君。
玩过移动端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.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账号...