近期使用tap事件为老夫带来了这样那样的问题,其中一个问题是解决 了点透还需要将原来一个个click变为tap,这样的话我们就抛弃了ie用户
当然可以做兼容,但是没人想动老代码 的,于是今天拿出了fastclick这个东西,
这是最近第四次发文说tap的点透事件,我们一直对解决 “点透”的蒙版 耿耿于怀,于是今天老大提出了一个库fastclick,最后证明解决 了我们的问题
而且click不必替换为tap了,于是我们老大就语重心长的对我说了一句,你们就误我吧,我邮件 都发出去了......
于是我下午就在看fastclick这个库,看看是不是能解决 我们的问题,于是我们开始吧
读fastclick源码
尼玛使用太简单了,直接一句:
FastClick.attach(document.body);
于是所有的click响应速度直接提升,刚刚的!什么input获取 焦点的问题也解决 了!!!尼玛如果真的可以的话,原来改页面 的同事肯定会啃了我
一步步来,我们跟进去,入口就是attach方法 :
FastClick.attach = function(layer) {
'use strict';
return new FastClick(layer);
};
这个兄弟不过实例化了下代码 ,所以我们还要看我们的构造函数 :
function FastClick(layer) {'use strict';var oldOnClick,self = this; this.trackingClick = false; this.trackingClickStart = 0; this.targetElement = null; this.touchStartX = 0; this.touchStartY = 0; this.lastTouchIdentifier = 0; this.touchBoundary = 10; this.layer = layer; if (!layer || !layer.nodeType) { throw new TypeError('Layer must be a document node'); } this.onClick = function() { return FastClick.prototype.onClick.apply(self,arguments); }; this.onMouse = function() { return FastClick.prototype.onMouse.apply(self,arguments); }; this.onTouchStart = function() { return FastClick.prototype.onTouchStart.apply(self,arguments); }; this.onTouchMove = function() { return FastClick.prototype.onTouchMove.apply(self,arguments); }; this.onTouchEnd = function() { return FastClick.prototype.onTouchEnd.apply(self,arguments); }; this.onTouchCancel = function() { return FastClick.prototype.onTouchCancel.apply(self,arguments); }; if (FastClick.notNeeded(layer)) { return; } if (this.deviceIsAndroid) { layer.addEventListener('mou
SEO ver',this.onMouse,true); layer.addEventListener('mousedown',true); layer.addEventListener('mouseup',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); 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,capture); } }; layer.addEventListener = function(type,capture) { var adv = Node.prototype.addEventListener; if (type === 'click') { adv.call(layer,callback.hijacked || (callback.hijacked = function(event) { if (!event.propagationStopped) { callback(event); } }),capture); } else { adv.call(layer,capture); } }; } if (typeof layer.onclick === 'function') { oldOnClick = layer.onclick; layer.addEventListener('click',function(event) {oldOnClick(event);},false);layer.onclick = null;}}
看看这段代码 ,上面很多属性 干了什么事情我也不知道......于是忽略了
if (!layer || !layer.nodeType) {
throw new TypeError('Layer must be a document node');
}
其中这里要注意,我们必须传入一个节点给构造函数 ,否则会出问题
然后这个家伙将一些基本的鼠标事件注册 在自己的属性 方法 上了,具体是干神马的我们后面再说
在后面点有个notNeeded方法 :
FastClick.notNeeded = function(layer) { 'use strict'; var
Meta Viewport; if (typeof window.ontouchstart === 'undefined') { return true; } if ((/Chrome\/[0-9]+/).test(navigator.userAgent)) { if (FastClick.prototype.deviceIsAndroid) {
Meta Viewport = document.querySelector('
Meta [name=viewport]'); if (
Meta Viewport &&
Meta Viewport.content.indexOf('user-scalable=no') !== -1) { return true; } } else { return true; } } if (layer.style.msTouchAction === 'none') { return true; } return false;};
这个方法 用于判断是否需要用到fastclick,注释的意思不太明白,我们看看代码 吧
首先一句:
if (typeof window.ontouchstart === 'undefined') {
return true;
}
如果不支持 touchstart事件的话,返回true
PS:现在的只管感受就是fastclick应该也是以touch事件模拟的,但是其没有点透问题
后面还判断了android的一些问题,我这里就不关注了,意思应该就是支持 touch才能支持 吧,于是回到主干代码
主干代码 中,我们看到,如果浏览器不支持 touch事件或者其它问题就直接跳出了
然后里面有个deviceIsAndroid的属性 ,我们跟去看看(其实不用看也知道是判断是否是android设备)
FastClick.prototype.deviceIsAndroid = navigator.userAgent.indexOf('Android') > 0;
绑定事件
好了,这家伙开始绑定注册 事件了,至此还未看出异样
if (this.deviceIsAndroid) { layer.addEventListener('mou
SEO ver',true);}layer.addEventListener('click',true);layer.addEventListener('touchstart',false);layer.addEventListener('touchmove',false);layer.addEventListener('touchend',false);layer.addEventListener('touchcancel',false);
具体的事件函数 在前面被重写了,我们暂时不管他,继续往后面看先(话说,这家伙绑定的事件够多的)
stopImmediatePropagation
完了多了一个属性 :
阻止当前事件的冒泡行为并且阻止当前事件所在元素上的所有相同类型事件的事件处理函数 的继续执行.
如果某个元素有多个相同类型事件的事件监听函数 ,则当该类型的事件触发时,多个事件监听函数 将按照顺序依次执行.如果某个监听函数 执行了 event.stopImmediatePropagation()方法 ,则除了该事件的冒泡行为被阻止之外(event.stopPropagation方法 的作用),该元素绑定的其余相同类型事件的监听函数 的执行也将被阻止.
if (!Event.prototype.stopImmediatePropagation) { layer.removeEventListener = function(type,capture); } };}
然后这家伙重新定义了下注册 与注销事件的方法 ,
我们先看注册 事件,其中用到了Node的addEventListener,这个Node是个什么呢?
由此观之,Node是一个系统属性 ,代表我们的节点吧,所以这里重写了注销的事件
这里,我们发现,其实他只对click进行了特殊处理
adv.call(layer,callback.hijacked || (callback.hijacked = function(event) {
if (!event.propagationStopped) {
callback(event);
}
}),capture);
其中有个hijacked劫持是干神马的就暂时不知道了,估计是在中间是否改写的意思吧
然后这里重写写了下,hijacked估计是一个方法 ,就是为了阻止在一个dom上注册 多次事件多次执行的情况而存在的吧
注销和注册 差不多我们就不管了,到此我们其实重写了我们传入dom的注册 注销事件了,好像很厉害的样子,意思以后这个dom调用 click事件用的是我们的,当然这只是我暂时的判断,具体还要往下读,而且我觉得现在的判断不靠谱,于是我们继续吧
我们注销事件时候可以用addEventListener 或者 dom.onclick=function(){},所以这里有了下面的代码 :
if (typeof layer.onclick === 'function') {
oldOnClick = layer.onclick;
layer.addEventListener('click',function(event) {
oldOnClick(event);
},false);
layer.onclick = null;
}
此处,他的主干流程居然就完了,意思是他所有的逻辑就在这里了,不论入口还是出口应该就是事件注册 了,于是我们写个代码 来看看
测试入口
我们来这个断点看看我们点击后干了什么,我们现在点击按钮1会为按钮2注册 事件:
但是很遗憾,我们在电脑上不能测试,所以增加 了我们读代码 的困难,在手机上测试后,发现按钮2响应很快,但是这里有点看不出问题
最后alert了一个!Event.prototype.stopImmediatePropagation发现手机和电脑都是false,所以我们上面搞的东西暂时无用
FastClick.prototype.onClick = function (event) { 'use strict'; var permitted; alert('终于尼玛进来了'); if (this.trackingClick) { this.targetElement = null; this.trackingClick = false; return true; } if (event.target.type === 'submit' && event.detail === 0) { return true; } permitted = this.onMouse(event); if (!permitted) { this.targetElement = null; } return permitted;};
然后我们终于进来了,现在我们需要知道什么是trackingClick 了
/**
* Whether a click is currently being tracked.
* @type Boolean
*/
this.trackingClick = false;
我们最初这个属性 是false,但是到这里就设置为true了,就直接退出 了,说明绑定事件终止,算了这个我们暂时不关注,我们干点其它的,
因为,我觉得重点还是应该在touch事件上
PS:到这里,我们发现这个库应该不只是将click加快,而是所有的响应都加快了
我在各个事件部分log出来东西,发现有click的地方都只执行了touchstart与touchend,于是至此,我觉得我的观点成立
他使用touch事件模拟量click,于是我们就只跟进这一块就好:
FastClick.prototype.onTouchStart = function (event) { 'use strict'; var targetElement,touch,selection; log('touchstart'); if (event.targetTouches.length > 1) { return true; } targetElement = this.getTargetElementFromEventTarget(event.target); touch = event.targetTouches[0]; if (this.deviceIsIOS) { selection = window.getSelection(); if (selection.rangeCount && !selection.isCollapsed) { return true; } if (!this.deviceIsIOS4) { if (touch.identifier === this.lastTouchIdentifier) { event.preventDefault(); return false; } this.lastTouchIdentifier = touch.identifier; this.updateScrollParent(targetElement); } } this.trackingClick = true; this.trackingClickStart = event.timeStamp; this.targetElement = targetElement; this.touchStartX = touch.pageX; this.touchStartY = touch.pageY; if ((event.timeStamp - this.lastClickTime) < 200) { event.preventDefault(); } return true;};
其中用到了一个方法 :
FastClick.prototype.getTargetElementFromEventTarget = function (eventTarget) {
'use strict';
if (eventTarget.nodeType === Node.TEXT_NODE) {
return eventTarget.parentNode;
}
return eventTarget;
};
他是获取 我们当前touchstart的元素
然后将鼠标的信息记录了下来,他记录鼠标信息主要在后面touchend时候根据x、y判断是否为click
是ios情况下还搞了一些事情,我这里跳过去了
然后这里记录了一些事情就跳出去了,没有特别的事情,现在我们进入我们的出口touchend
FastClick.prototype.onTouchEnd = function (event) { 'use strict'; var forElement,trackingClickStart,targetTagName,scrollParent,targetElement = this.targetElement; log('touchend'); if (!this.trackingClick) { return true; } if ((event.timeStamp - this.lastClickTime) < 200) { this.cancelNextClick = true; return true; } this.lastClickTime = event.timeStamp; trackingClickStart = this.trackingClickStart; this.trackingClick = false; this.trackingClickStart = 0; if (this.deviceIsIOSWithBadTarget) { touch = event.changedTouches[0]; targetElement = document.elementFromPoint(touch.pageX - window.pageXOffset,touch.pageY - window.pageYOffset) || targetElement; targetElement.fastClickScrollParent = this.targetElement.fastClickScrollParent; } targetTagName = targetElement.tagName.toLowerCase(); if (targetTagName === 'label') { forElement = this.findControl(targetElement); if (forElement) { this.focus(targetElement); if (this.deviceIsAndroid) { return false; } targetElement = forElement; } } else if (this.needsFocus(targetElement)) { if ((event.timeStamp - trackingClickStart) > 100 || (this.deviceIsIOS && window.top !== window && targetTagName === 'input')) { this.targetElement = null; return false; } this.focus(targetElement); if (!this.deviceIsIOS4 || targetTagName !== 'select') { this.targetElement = null; event.preventDefault(); } return false; } if (this.deviceIsIOS && !this.deviceIsIOS4) { scrollParent = targetElement.fastClickScrollParent; if (scrollParent && scrollParent.fastClickLastScrollTop !== scrollParent.scrollTop) { return true; } } if (!this.needsClick(targetElement)) { event.preventDefault(); this.sendClick(targetElement,event); } return false;};
这个家伙洋洋洒洒干了许多事情
这里纠正一个错误 ,他onclick那些东西现在也执行了......可能是我屏幕有变化(滑动)导致
if ((event.timeStamp - this.lastClickTime) < 200) {
this.cancelNextClick = true;
return true;
}
这个代码 很关键,我们首次点击会执行下面的逻辑,如果连续点击就直接完蛋,下面的逻辑丫的不执行了......
这个不执行了,那么这个劳什子又干了什么事情呢?
事实上下面就没逻辑了,意思是如果确实点击过快,两次点击只会执行一次,这个阀值为200ms,这个暂时看来是没有问题的
好了,我们继续往下走,于是我意识到又到了一个关键点
因为我们用tap事件不能使input获得焦点,但是fastclick却能获得焦点,这里也许是一个关键,我们来看看几个与获取 焦点有关的函数
FastClick.prototype.focus = function (targetElement) { 'use strict'; var length; if (this.deviceIsIOS && targetElement.setSelectionRange) { length = targetElement.value.length; targetElement.setSelectionRange(length,length); } else { targetElement.focus(); }};
setSelectionRange是我们的关键,也许他是这样获取 焦点的......具体我还要下来测试,留待下次处理吧
然后下面如果时间间隔过长,代码 就不认为操作的是同一dom结构了
最后迎来了本次的关键:sendClick,无论是touchend还是onMouse都会汇聚到这里
FastClick.prototype.sendClick = function (targetElement,event) { 'use strict'; 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('click',true,window,1,touch.screenX,touch.screenY,touch.clientX,touch.clientY,false,null); clickEvent.forwardedTouchEvent = true; targetElement.dispatchEvent(clickEvent);};
他创建了一个鼠标事件,然后dispatchEvent事件(这个与fireEvent类似)
//document上绑定
自定义 事件ondataavailabledocument.addEventListener('ondataavailable',function (event) {alert(event.eventType);},false);var obj = document.getElementById("obj");//obj元素上绑定click事件obj.addEventListener('click',false);//
调用 document对象的 createEvent
方法 得到一个event的对象实例。var event = document.createEvent('HTMLEvents');// initEvent接受3个参数:// 事件类型,是否冒泡,是否阻止浏览器的默认行为event.initEvent("ondataavailable",true);event.eventType = 'message';//触发document上绑定的
自定义 事件ondataavailabledocument.dispatchEvent(event);var event1 = document.createEvent('HTMLEvents');event1.initEvent("click",true);event1.eventType = 'message';//触发obj元素上绑定click事件document.getElementById("test").onclick = function () {obj.dispatchEvent(event1);};
至此,我们就知道了,我们为dom先绑定了鼠标事件,然后touchend时候触发了,而至于为什么本身注册 的click未触发就要回到上面代码 了
解决 “点透”(成果)
有了这个思路,我们来试试我们抽象出来的代码 :
<
Meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1">