这篇文章主要介绍了浅谈fastclick 填坑及源码解析,现在分享给大家,也给大家做个参考。
最近产品妹子提出了一个体验issue —— 用 ios 在手q阅读书友交流区发表书评时,光标点击总是不好定位到正确的位置:
如上图,具体表现是较快点击时,光标总会跳到 textarea 内容的尾部。只有当点击停留时间较久一点(比如超过150ms)才能把光标正常定位到正确的位置。
一开始我以为是 ios 原生的交互问题没太在意,但后来发现访问某些页面又是没有这种奇怪体验的。
然后怀疑是否 js 注册了某些事件导致的问题,于是试着把业务模块移除了再跑一遍,发现问题照旧。
于是只好继续做排除法,把页面上的一些库一点点移掉再运行页面,结果发现捣乱的小鬼果然是嫌疑最大的 fastclick。
然后呢,我试着按api所说,给 textarea 加上一个名为“needsclick”的类名,希望能绕过 fastclick 的处理直接走原生点击事件,结果讶异地发现屁用没有。。。
对此感谢后面我们小组的 kindeng 童鞋帮忙研究了下并提供了解决方案,不过我还想进一步研究到底是什么原因导致了这个坑、fastclick 对我的页面做了神马~
所以昨晚花了点时间一口气把源码都蹂躏了一遍。
这会是一篇很长的文章,但会是注释非常详尽的剖析文。
文章带分析的源码我也挂在我的 github 仓库上了,有兴趣的童鞋可以去下载来看。
闲话不多说,咱们开始深入 fastclick 源码阵营。
我们知道,注册一个 fastclick 事件非常简单,它是这样的:
if ('addeventlistener' in document) {
document.addeventlistener('domcontentloaded', function() {
var fc = fastclick.attach(document.body); //生成实例
}, false);
}
所以我们从这里着手,打开源码看下 fastclick .attach 方法:
fastclick.attach = function(layer, options) {
return new fastclick(layer, options);
};
这里返回了一个 fastclick 实例,所以咱们拉到前面看看 fastclick 构造函数:
function fastclick(layer, options) {
var oldonclick;
options = options || {};
//定义了一些参数...
//如果是属于不需要处理的元素类型,则直接返回
if (fastclick.notneeded(layer)) {
return;
}
//语法糖,兼容一些用不了 function.prototype.bind 的旧安卓
//所以后面不走 layer.addeventlistener('click', this.onclick.bind(this), true);
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);
}
//安卓则做额外处理
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);
// 兼容不支持 stopimmediatepropagation 的浏览器(比如 android 2)
if (!event.prototype.stopimmediatepropagation) {
layer.removeeventlistener = function(type, callback, capture) {
var rmv = node.prototype.removeeventlistener;
if (type === 'click') {
rmv.call(layer, type, callback.hijacked || callback, capture);
} else {
rmv.call(layer, type, callback, capture);
}
};
layer.addeventlistener = function(type, callback, capture) {
var adv = node.prototype.addeventlistener;
if (type === 'click') {
//留意这里 callback.hijacked 中会判断 event.propagationstopped 是否为真来确保(安卓的onmouse事件)只执行一次
//在 onmouse 事件里会给 event.propagationstopped 赋值 true
adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) {
if (!event.propagationstopped) {
callback(event);
}
}), capture);
} else {
adv.call(layer, type, callback, capture);
}
};
}
// 如果layer直接在dom上写了 onclick 方法,那我们需要把它替换为 addeventlistener 绑定形式
if (typeof layer.onclick === 'function') {
oldonclick = layer.onclick;
layer.addeventlistener('click', function(event) {
oldonclick(event);
}, false);
layer.onclick = null;
}
}
在初始通过 fastclick.notneeded 方法判断是否需要做后续的相关处理:
//如果是属于不需要处理的元素类型,则直接返回
if (fastclick.notneeded(layer)) {
return;
}
我们看下这个 fastclick.notneeded 都做了哪些判断:
//是否没必要使用到 fastclick 的检测
fastclick.notneeded = function(layer) {
var metaviewport;
var chromeversion;
var blackberryversion;
var firefoxversion;
// 不支持触摸的设备
if (typeof window.ontouchstart === 'undefined') {
return true;
}
// 获取chrome版本号,若非chrome则返回0
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;
}
// 安卓chrome 32 及以上版本,若带有 width=device-width 的 meta 标签也是无需 fastclick 的
if (chromeversion > 31 && document.documentelement.scrollwidth <= window.outerwidth) {
return true;
}
}
// 其它的就肯定是桌面级的 chrome 了,更不需要 fastclick 啦
} else {
return true;
}
}
if (deviceisblackberry10) { //黑莓,和上面安卓同理,就不写注释了
blackberryversion = navigator.useragent.match(/version\/([0-9]*)\.([0-9]*)/);
if (blackberryversion[1] >= 10 && blackberryversion[2] >= 3) {
metaviewport = document.queryselector('meta[name=viewport]');
if (metaviewport) {
if (metaviewport.content.indexof('user-scalable=no') !== -1) {
return true;
}
if (document.documentelement.scrollwidth <= window.outerwidth) {
return true;
}
}
}
}
// 带有 -ms-touch-action: none / manipulation 特性的 ie10 会禁用双击放大,也没有 300ms 时延
if (layer.style.mstouchaction === 'none' || layer.style.touchaction === 'manipulation') {
return true;
}
// firefox检测,同上
firefoxversion = +(/firefox\/([0-9]+)/.exec(navigator.useragent) || [,0])[1];
if (firefoxversion >= 27) {
metaviewport = document.queryselector('meta[name=viewport]');
if (metaviewport && (metaviewport.content.indexof('user-scalable=no') !== -1 || document.documentelement.scrollwidth <= window.outerwidth)) {
return true;
}
}
// ie11 推荐使用没有“-ms-”前缀的 touch-action 样式特性名
if (layer.style.touchaction === 'none' || layer.style.touchaction === 'manipulation') {
return true;
}
return false;
};
基本上都是一些能禁用 300ms 时延的浏览器嗅探,它们都没必要使用 fastclick,所以会返回 true 回构造函数停止下一步执行。
由于安卓手q的 ua 会被匹配到 /chrome\/([0-9]+)/,故带有 'user-scalable=no' meta 标签的安卓手q页会被 fastclick 视为无需处理页。
这也是为何在安卓手q里没有开头提及问题的原因。
我们继续看构造函数,它直接给 layer(即body)添加了click、touchstart、touchmove、touchend、touchcancel(若是安卓还有 mouseover、mousedown、mouseup)事件监听:
//安卓则做额外处理
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);
注意在这段代码上面还利用了 bind 方法做了处理,这些事件回调中的 this 都会变成 fastclick 实例上下文。
另外还得留意,onclick 事件以及安卓的额外处理部分都是走的捕获监听。
咱们分别看看这些事件回调分别都做了什么。
1. this.ontouchstart
fastclick.prototype.ontouchstart = function(event) {
var targetelement, touch, selection;
// 多指触控的手势则忽略
if (event.targettouches.length > 1) {
return true;
}
targetelement = this.gettargetelementfromeventtarget(event.target); //一些较老的浏览器,target 可能会是一个文本节点,得返回其dom节点
touch = event.targettouches[0];
if (deviceisios) { //ios处理
// 若用户已经选中了一些内容(比如选中了一段文本打算复制),则忽略
selection = window.getselection();
if (selection.rangecount && !selection.iscollapsed) {
return true;
}
if (!deviceisios4) { //是否ios4
//怪异特性处理——若click事件回调打开了一个alert/confirm,用户下一次tap页面的其它地方时,新的touchstart和touchend
//事件会拥有同一个touch.identifier(新的 touch event 会跟上一次触发alert点击的 touch event 一样),
//为避免将新的event当作之前的event导致问题,这里需要禁用事件
//另外chrome的开发工具启用'emulate touch events'后,ios ua下的 identifier 会变成0,所以要做容错避免调试过程也被禁用事件了
if (touch.identifier && touch.identifier === this.lasttouchidentifier) {
event.preventdefault();
return false;
}
this.lasttouchidentifier = touch.identifier;
// 如果target是一个滚动容器里的一个子元素(使用了 -webkit-overflow-scrolling: touch) ,而且满足:
// 1) 用户非常快速地滚动外层滚动容器
// 2) 用户通过tap停止住了这个快速滚动
// 这时候最后的'touchend'的event.target会变成用户最终手指下的那个元素
// 所以当快速滚动开始的时候,需要做检查target是否滚动容器的子元素,如果是,做个标记
// 在touchend时检查这个标记的值(滚动容器的scrolltop)是否改变了,如果是则说明页面在滚动中,需要取消fastclick处理
this.updatescrollparent(targetelement);
}
}
this.trackingclick = true; //做个标志表示开始追踪click事件了
this.trackingclickstart = event.timestamp; //标记下touch事件开始的时间戳
this.targetelement = targetelement;
//标记touch起始点的页面偏移值
this.touchstartx = touch.pagex;
this.touchstarty = touch.pagey;
// this.lastclicktime 是在 touchend 里标记的事件时间戳
// this.tapdelay 为常量 200 (ms)
// 此举用来避免 phantom 的双击(200ms内快速点了两次)触发 click
// 反正200ms内的第二次点击会禁止触发其默认事件
if ((event.timestamp - this.lastclicktime) < this.tapdelay) {
event.preventdefault();
}
return true;
};
顺道看下这里的 this.updatescrollparent:
/**
* 检查target是否一个滚动容器里的子元素,如果是则给它加个标记
*/
fastclick.prototype.updatescrollparent = function(targetelement) {
var scrollparent, parentelement;
scrollparent = targetelement.fastclickscrollparent;
if (!scrollparent || !scrollparent.contains(targetelement)) {
parentelement = targetelement;
do {
if (parentelement.scrollheight > parentelement.offsetheight) {
scrollparent = parentelement;
targetelement.fastclickscrollparent = parentelement;
break;
}
parentelement = parentelement.parentelement;
} while (parentelement);
}
// 给滚动容器加个标志fastclicklastscrolltop,值为其当前垂直滚动偏移
if (scrollparent) {
scrollparent.fastclicklastscrolltop = scrollparent.scrolltop;
}
};
另外要注意的是,在 ontouchstart 里被标记为 true 的 this.trackingclick 属性,都会在其它事件回调(比如 ontouchmove )的开头做检测,如果没被赋值过,则直接忽略:
if (!this.trackingclick) {
return true;
}
当然在 ontouchend 事件里会把它重置为 false。
2. this.ontouchmove
这段代码量好少:
fastclick.prototype.ontouchmove = function(event) {
//不是需要被追踪click的事件则忽略
if (!this.trackingclick) {
return true;
}
// 如果target突然改变了,或者用户其实是在移动手势而非想要click
// 则应该清掉this.trackingclick和this.targetelement,告诉后面的事件你们也不用处理了
if (this.targetelement !== this.gettargetelementfromeventtarget(event.target) || this.touchhasmoved(event)) {
this.trackingclick = false;
this.targetelement = null;
}
return true;
};
看下这里用到的 this.touchhasmoved 原型方法:
//判断是否移动了
//this.touchboundary是常量,值为10
//如果touch已经移动了10个偏移量单位,则应当作为移动事件处理而非click事件
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;
};
3. ontouchend
fastclick.prototype.ontouchend = function(event) {
var forelement, trackingclickstart, targettagname, scrollparent, touch, targetelement = this.targetelement;
if (!this.trackingclick) {
return true;
}
// 避免 phantom 的双击(200ms内快速点了两次)触发 click
// 我们在 ontouchstart 里已经做过一次判断了(仅仅禁用默认事件),这里再做一次判断
if ((event.timestamp - this.lastclicktime) < this.tapdelay) {
this.cancelnextclick = true; //该属性会在 onmouse 事件中被判断,为true则彻底禁用事件和冒泡
return true;
}
//this.taptimeout是常量,值为700
//识别是否为长按事件,如果是(大于700ms)则忽略
if ((event.timestamp - this.trackingclickstart) > this.taptimeout) {
return true;
}
// 得重置为false,避免input事件被意外取消
// 例子见 https://github.com/ftlabs/fastclick/issues/156
this.cancelnextclick = false;
this.lastclicktime = event.timestamp; //标记touchend时间,方便下一次的touchstart做双击校验
trackingclickstart = this.trackingclickstart;
//重置 this.trackingclick 和 this.trackingclickstart
this.trackingclick = false;
this.trackingclickstart = 0;
// ios 6.0-7.*版本下有个问题 —— 如果layer处于transition或scroll过程,event所提供的target是不正确的
// 所以咱们得重找 targetelement(这里通过 document.elementfrompoint 接口来寻找)
if (deviceisioswithbadtarget) { //ios 6.0-7.*版本
touch = event.changedtouches[0]; //手指离开前的触点
// 有些情况下 elementfrompoint 里的参数是预期外/不可用的, 所以还得避免 targetelement 为 null
targetelement = document.elementfrompoint(touch.pagex - window.pagexoffset, touch.pagey - window.pageyoffset) || targetelement;
// target可能不正确需要重找,但fastclickscrollparent是不会变的
targetelement.fastclickscrollparent = this.targetelement.fastclickscrollparent;
}
targettagname = targetelement.tagname.tolowercase();
if (targettagname === 'label') { //是label则激活其指向的组件
forelement = this.findcontrol(targetelement);
if (forelement) {
this.focus(targetelement);
//安卓直接返回(无需合成click事件触发,因为点击和激活元素不同,不存在点透)
if (deviceisandroid) {
return false;
}
targetelement = forelement;
}
} else if (this.needsfocus(targetelement)) { //非label则识别是否需要focus的元素
//手势停留在组件元素时长超过100ms,则置空this.targetelement并返回
//(而不是通过调用this.focus来触发其聚焦事件,走的原生的click/focus事件触发流程)
//这也是为何文章开头提到的问题中,稍微久按一点(超过100ms)textarea是可以把光标定位在正确的地方的原因
//另外ios下有个意料之外的bug——如果被点击的元素所在文档是在iframe中的,手动调用其focus的话,
//会发现你往其中输入的text是看不到的(即使value做了更新),so这里也直接返回
if ((event.timestamp - trackingclickstart) > 100 || (deviceisios && window.top !== window && targettagname === 'input')) {
this.targetelement = null;
return false;
}
this.focus(targetelement);
this.sendclick(targetelement, event); //立即触发其click事件,而无须等待300ms
//ios4下的 select 元素不能禁用默认事件(要确保它能被穿透),否则不会打开select目录
//有时候 ios6/7 下(voiceover开启的情况下)也会如此
if (!deviceisios || targettagname !== 'select') {
this.targetelement = null;
event.preventdefault();
}
return false;
}
if (deviceisios && !deviceisios4) {
// 滚动容器的垂直滚动偏移改变了,说明是容器在做滚动而非点击,则忽略
scrollparent = targetelement.fastclickscrollparent;
if (scrollparent && scrollparent.fastclicklastscrolltop !== scrollparent.scrolltop) {
return true;
}
}
// 查看元素是否无需处理的白名单内(比如加了名为“needsclick”的class)
// 不是白名单的则照旧预防穿透处理,立即触发合成的click事件
if (!this.needsclick(targetelement)) {
event.preventdefault();
this.sendclick(targetelement, event);
}
return false;
};
这段比较长,我们主要看这段:
} else if (this.needsfocus(targetelement)) { //非label则识别是否需要focus的元素
//手势停留在组件元素时长超过100ms,则置空this.targetelement并返回
//(而不是通过调用this.focus来触发其聚焦事件,走的原生的click/focus事件触发流程)
//这也是为何文章开头提到的问题中,稍微久按一点(超过100ms)textarea是可以把光标定位在正确的地方的原因
//另外ios下有个意料之外的bug——如果被点击的元素所在文档是在iframe中的,手动调用其focus的话,
//会发现你往其中输入的text是看不到的(即使value做了更新),so这里也直接返回
if ((event.timestamp - trackingclickstart) > 100 || (deviceisios && window.top !== window && targettagname === 'input')) {
this.targetelement = null;
return false;
}
this.focus(targetelement);
this.sendclick(targetelement, event); //立即触发其click事件,而无须等待300ms
//ios4下的 select 元素不能禁用默认事件(要确保它能被穿透),否则不会打开select目录
//有时候 ios6/7 下(voiceover开启的情况下)也会如此
if (!deviceisios || targettagname !== 'select') {
this.targetelement = null;
event.preventdefault();
}
return false;
}
其中 this.needsfocus 用于判断给定元素是否需要通过合成click事件来模拟聚焦:
//判断给定元素是否需要通过合成click事件来模拟聚焦
fastclick.prototype.needsfocus = function(target) {
switch (target.nodename.tolowercase()) {
case 'textarea':
return true;
case 'select':
return !deviceisandroid; //ios下的select得走穿透点击才行
case 'input':
switch (target.type) {
case 'button':
case 'checkbox':
case 'file':
case 'image':
case 'radio':
case 'submit':
return false;
}
return !target.disabled && !target.readonly;
default:
//带有名为“bneedsfocus”的class则返回true
return (/\bneedsfocus\b/).test(target.classname);
}
};
另外这段说明了为何稍微久按一点(超过100ms)textarea ,我们是可以把光标定位在正确的地方(会绕过后面调用 this.focus 的方法):
//手势停留在组件元素时长超过100ms,则置空this.targetelement并返回
//(而不是通过调用this.focus来触发其聚焦事件,走的原生的click/focus事件触发流程)
//这也是为何文章开头提到的问题中,稍微久按一点(超过100ms)textarea是可以把光标定位在正确的地方的原因
//另外ios下有个意料之外的bug——如果被点击的元素所在文档是在iframe中的,手动调用其focus的话,
//会发现你往其中输入的text是看不到的(即使value做了更新),so这里也直接返回
if ((event.timestamp - trackingclickstart) > 100 || (deviceisios && window.top !== window && targettagname === 'input')) {
this.targetelement = null;
return false;
}
接着咱们看看这两行很重要的代码:
this.focus(targetelement);
this.sendclick(targetelement, event); //立即触发其click事件,而无须等待300ms
所涉及的两个原型方法分别为:
⑴ this.focus
fastclick.prototype.focus = function(targetelement) {
var length;
// 组件建议通过setselectionrange(selectionstart, selectionend)来设定光标范围(注意这样还没有聚焦
// 要等到后面触发 sendclick 事件才会聚焦)
// 另外 ios7 下有些input元素(比如 date datetime month) 的 selectionstart 和 selectionend 特性是没有整型值的,
// 导致会抛出一个关于 setselectionrange 的模糊错误,它们需要改用 focus 事件触发
if (deviceisios && targetelement.setselectionrange && targetelement.type.indexof('date') !== 0 && targetelement.type !== 'time' && targetelement.type !== 'month') {
length = targetelement.value.length;
targetelement.setselectionrange(length, length);
} else {
//直接触发其focus事件
targetelement.focus();
}
};
注意,我们点击 textarea 时调用了该方法,它通过 targetelement.setselectionrange(length, length) 决定了光标的位置在内容的尾部(但注意,这时候还没聚焦!!!)。
⑵ this.sendclick
真正让 textarea 聚焦的是这个方法,它合成了一个 click 方法立刻在textarea元素上触发导致聚焦:
//合成一个click事件并在指定元素上触发
fastclick.prototype.sendclick = function(targetelement, event) {
var clickevent, touch;
// 在一些安卓机器中,得让页面所存在的 activeelement(聚焦的元素,比如input)失焦,否则合成的click事件将无效
if (document.activeelement && document.activeelement !== targetelement) {
document.activeelement.blur();
}
touch = event.changedtouches[0];
// 合成(synthesise) 一个 click 事件
// 通过一个额外属性确保它能被追踪(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; // fastclick的内部变量,用来识别click事件是原生还是合成的
targetelement.dispatchevent(clickevent); //立即触发其click事件
};
fastclick.prototype.determineeventtype = function(targetelement) {
//安卓设备下 select 无法通过合成的 click 事件被展开,得改为 mousedown
if (deviceisandroid && targetelement.tagname.tolowercase() === 'select') {
return 'mousedown';
}
return 'click';
};
经过这么一折腾,咱们轻点 textarea 后,光标就自然定位到其内容尾部去了。但是这里有个问题——排在 touchend 后的 focus 事件为啥没被触发呢?
如果 focus 事件能被触发的话,那肯定能重新定位光标到正确的位置呀。
咱们看下面这段:
//ios4下的 select 元素不能禁用默认事件(要确保它能被穿透),否则不会打开select目录
//有时候 ios6/7 下(voiceover开启的情况下)也会如此
if (!deviceisios || targettagname !== 'select' ) {
this.targetelement = null;
event.preventdefault();
}
通过 preventdefault 的阻挡,textarea 自然再也无法拥抱其 focus 宝宝了~
于是乎,我们在这里做个改动就能修复这个问题:
var _istextinput = function(){
return targettagname === 'textarea' || (targettagname === 'input' && targetelement.type === 'text');
};
if ((!deviceisios || targettagname !== 'select') && !_istextinput()) {
this.targetelement = null;
event.preventdefault();
}
或者:
if (!deviceisios4 || targettagname !== 'select') {
this.targetelement = null;
//给textarea加上“needsclick”的class
if((!/\bneedsclick\b/).test(targetelement.classname)){
event.preventdefault();
}
}
这里要吐槽下的是,fastclick 把 this.needsclick 放到了 ontouchend 末尾去执行,才导致前面说的加上了“needsclick”类名也无效的问题。
虽然问题原因找到也解决了,但咱们还是继续看剩下的部分吧。
4. onmouse 和 onclick
//用于决定是否允许穿透事件(触发layer的click默认事件)
fastclick.prototype.onmouse = function(event) {
// touch事件一直没触发
if (!this.targetelement) {
return true;
}
if (event.forwardedtouchevent) { //触发的click事件是合成的
return true;
}
// 编程派生的事件所对应元素事件可以被允许
// 确保其没执行过 preventdefault 方法(event.cancelable 不为 true)即可
if (!event.cancelable) {
return true;
}
// 需要做预防穿透处理的元素,或者做了快速(200ms)双击的情况
if (!this.needsclick(this.targetelement) || this.cancelnextclick) {
//停止当前默认事件和冒泡
if (event.stopimmediatepropagation) {
event.stopimmediatepropagation();
} else {
// 不支持 stopimmediatepropagation 的设备(比如android 2)做标记,
// 确保该事件回调不会执行(见126行)
event.propagationstopped = true;
}
// 取消事件和冒泡
event.stoppropagation();
event.preventdefault();
return false;
}
//允许穿透
return true;
};
//click事件常规都是touch事件衍生来的,也排在touch后面触发。
//对于那些我们在touch事件过程没有禁用掉默认事件的event来说,我们还需要在click的捕获阶段进一步
//做判断决定是否要禁掉点击事件(防穿透)
fastclick.prototype.onclick = function(event) {
var permitted;
// 如果还有 trackingclick 存在,可能是某些ui事件阻塞了touchend 的执行
if (this.trackingclick) {
this.targetelement = null;
this.trackingclick = false;
return true;
}
// 依旧是对 ios 怪异行为的处理 —— 如果用户点击了ios模拟器里某个表单中的一个submit元素
// 或者点击了弹出来的键盘里的“go”按钮,会触发一个“伪”click事件(target是一个submit-type的input元素)
if (event.target.type === 'submit' && event.detail === 0) {
return true;
}
permitted = this.onmouse(event);
if (!permitted) { //如果点击是被允许的,将this.targetelement置空可以确保onmouse事件里不会阻止默认事件
this.targetelement = null;
}
//没有多大意义
return permitted;
};
//销毁fastclick所注册的监听事件。是给外部实例去调用的
fastclick.prototype.destroy = function() {
var layer = this.layer;
if (deviceisandroid) {
layer.removeeventlistener('mouseover', this.onmouse, true);
layer.removeeventlistener('mousedown', this.onmouse, true);
layer.removeeventlistener('mouseup', this.onmouse, true);
}
layer.removeeventlistener('click', this.onclick, true);
layer.removeeventlistener('touchstart', this.ontouchstart, false);
layer.removeeventlistener('touchmove', this.ontouchmove, false);
layer.removeeventlistener('touchend', this.ontouchend, false);
layer.removeeventlistener('touchcancel', this.ontouchcancel, false);
};
常规需要阻断点击事件的操作,我们在 touch 监听事件回调中已经做了处理,这里主要是针对那些 touch 过程(有些设备甚至可能并没有touch事件触发)没有禁用默认事件的 event 做进一步处理,从而决定是否触发原生的 click 事件(如果禁止是在 onmouse 方法里做的处理)。
小结
1. 在 fastclick 源码的 addeventlistener 回调事件中有很多的 return false/true。它们其实主要用于绕过后面的脚本逻辑,并没有其它意义(它是不会阻止默认事件的)。
所以千万别把 jquery 事件、或者 dom0 级事件回调中的 return false 概念,跟 addeventlistener 的混在一起了。
2. fastclick 的源码其实很简单,有很大部分不外乎对一些怪异行为做 hack,其核心理念不外乎是——捕获 target 事件,判断 target 是要解决点透问题的元素,就合成一个 click 事件在 target 上触发,同时通过 preventdefault 禁用默认事件。
3. fastclick 虽好,但也有一些坑,还是得按需求对其修改,那么了解其源码还是很有必要的。
上面是我整理给大家的,希望今后会对大家有帮助。
相关文章:
如何关闭vue计算属性自带的缓存功能,具体步骤有哪些?
利用vue2.0中swiper组件实现轮播(详细教程)
有关在vue中使用compass的具体方法?
以上就是详细讲解fastclick源码(详细教程)的详细内容。
