申明!!!最后发现判断有误,各位读读就好,正在研究中.....尼玛水太深了javascript
近期使用tap事件为老夫带来了这样那样的问题,其中一个问题是解决了点透还须要将原来一个个click变为tap,这样的话咱们就抛弃了ie用户
固然能够作兼容,可是没人想动老代码的,因而今天拿出了fastclick这个东西,html
这是最近第四次发文说tap的点透事件,咱们一直对解决“点透”的蒙版耿耿于怀,因而今天老大提出了一个库fastclick,最后证实解决了咱们的问题java
并且click没必要替换为tap了,因而咱们老大就语重心长的对我说了一句,大家就误我吧,我邮件都发出去了......node
因而我下午就在看fastclick这个库,看看是否是能解决咱们的问题,因而咱们开始吧android
尼玛使用太简单了,直接一句:ios
FastClick.attach(document.body);
因而全部的click响应速度直接提高,刚刚的!什么input获取焦点的问题也解决了!!!尼玛若是真的能够的话,原来改页面的同事确定会啃了我浏览器
一步步来,咱们跟进去,入口就是attach方法:app
1 FastClick.attach = function(layer) { 2 'use strict'; 3 return new FastClick(layer); 4 };
这个兄弟不过实例化了下代码,因此咱们还要看咱们的构造函数:dom
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('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); 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') { adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) { if (!event.propagationStopped) { callback(event); } }), capture); } else { adv.call(layer, type, callback, capture); } }; } if (typeof layer.onclick === 'function') { oldOnClick = layer.onclick; layer.addEventListener('click', function(event) { oldOnClick(event); }, false); layer.onclick = null; } }
看看这段代码,上面不少属性干了什么事情我也不知道......因而忽略了ide
1 if (!layer || !layer.nodeType) { 2 throw new TypeError('Layer must be a document node'); 3 }
其中这里要注意,咱们必须传入一个节点给构造函数,不然会出问题
而后这个家伙将一些基本的鼠标事件注册在本身的属性方法上了,具体是干神马的咱们后面再说
在后面点有个notNeeded方法:
FastClick.notNeeded = function(layer) { 'use strict'; var metaViewport; if (typeof window.ontouchstart === 'undefined') { return true; } if ((/Chrome\/[0-9]+/).test(navigator.userAgent)) { if (FastClick.prototype.deviceIsAndroid) { metaViewport = document.querySelector('meta[name=viewport]'); if (metaViewport && metaViewport.content.indexOf('user-scalable=no') !== -1) { return true; } } else { return true; } } if (layer.style.msTouchAction === 'none') { return true; } return false; };
这个方法用于判断是否须要用到fastclick,注释的意思不太明白,咱们看看代码吧
首先一句:
1 if (typeof window.ontouchstart === 'undefined') { 2 return true; 3 }
若是不支持touchstart事件的话,返回true
PS:如今的只管感觉就是fastclick应该也是以touch事件模拟的,可是其没有点透问题
后面还判断了android的一些问题,我这里就不关注了,意思应该就是支持touch才能支持吧,因而回到主干代码
主干代码中,咱们看到,若是浏览器不支持touch事件或者其它问题就直接跳出了
而后里面有个deviceIsAndroid的属性,咱们跟去看看(其实不用看也知道是判断是不是android设备)
FastClick.prototype.deviceIsAndroid = navigator.userAgent.indexOf('Android') > 0;
好了,这家伙开始绑定注册事件了,至此还未看出异样
if (this.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);
具体的事件函数在前面被重写了,咱们暂时无论他,继续日后面看先(话说,这家伙绑定的事件够多的)
完了多了一个属性:
阻止当前事件的冒泡行为而且阻止当前事件所在元素上的全部相同类型事件的事件处理函数的继续执行.
若是某个元素有多个相同类型事件的事件监听函数,则当该类型的事件触发时,多个事件监听函数将按照顺序依次执行.若是某个监听函数执行了 event.stopImmediatePropagation()方法,则除了该事件的冒泡行为被阻止以外(event.stopPropagation方法的做用),该元素绑定的其他相同类型事件的监听函数的执行也将被阻止.
<html> <head> <style> p { height: 30px; width: 150px; background-color: #ccf; } div {height: 30px; width: 150px; background-color: #cfc; } </style> </head> <body> <div> <p>paragraph</p> </div> <script> document.querySelector("p").addEventListener("click", function(event) { alert("我是p元素上被绑定的第一个监听函数"); }, false); document.querySelector("p").addEventListener("click", function(event) { alert("我是p元素上被绑定的第二个监听函数"); event.stopImmediatePropagation(); //执行stopImmediatePropagation方法,阻止click事件冒泡,而且阻止p元素上绑定的其余click事件的事件监听函数的执行. }, false); document.querySelector("p").addEventListener("click", function(event) { alert("我是p元素上被绑定的第三个监听函数"); //该监听函数排在上个函数后面,该函数不会被执行. }, false); document.querySelector("div").addEventListener("click", function(event) { alert("我是div元素,我是p元素的上层元素"); //p元素的click事件没有向上冒泡,该函数不会被执行. }, false); </script> </body> </html>
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') { adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) { if (!event.propagationStopped) { callback(event); } }), capture); } else { adv.call(layer, type, callback, capture); } }; }
而后这家伙从新定义了下注册与注销事件的方法,
咱们先看注册事件,其中用到了Node的addEventListener,这个Node是个什么呢?
由此观之,Node是一个系统属性,表明咱们的节点吧,因此这里重写了注销的事件
这里,咱们发现,其实他只对click进行了特殊处理
1 adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) { 2 if (!event.propagationStopped) { 3 callback(event); 4 } 5 }), capture);
其中有个hijacked劫持是干神马的就暂时不知道了,估计是在中间是否改写的意思吧
而后这里重写写了下,hijacked估计是一个方法,就是为了阻止在一个dom上注册屡次事件屡次执行的状况而存在的吧
注销和注册差很少咱们就无论了,到此咱们其实重写了咱们传入dom的注册注销事件了,好像很厉害的样子,意思之后这个dom调用click事件用的是咱们的,固然这只是我暂时的判断,具体还要往下读,并且我以为如今的判断不靠谱,因而咱们继续吧
咱们注销事件时候能够用addEventListener 或者 dom.onclick=function(){},因此这里有了下面的代码:
1 if (typeof layer.onclick === 'function') { 2 oldOnClick = layer.onclick; 3 layer.addEventListener('click', function(event) { 4 oldOnClick(event); 5 }, false); 6 layer.onclick = null; 7 }
此处,他的主干流程竟然就完了,意思是他全部的逻辑就在这里了,不论入口仍是出口应该就是事件注册了,因而咱们写个代码来看看
1 <input type="button" id="addEvent" value="addevent"> 2 <input type="button" id="addEvent1" value="addevent1"> 3 4 $('#addEvent').click(function () { 5 var dom = $('#addEvent1')[0] 6 dom.addEventListener('click', function () { 7 alert('') 8 var s = ''; 9 }) 10 });
咱们来这个断点看看咱们点击后干了什么,咱们如今点击按钮1会为按钮2注册事件:
可是很遗憾,咱们在电脑上不能测试,因此增长了咱们读代码的困难,在手机上测试后,发现按钮2响应很快,可是这里有点看不出问题
最后alert了一个!Event.prototype.stopImmediatePropagation发现手机和电脑都是false,因此咱们上面搞的东西暂时无用
1 FastClick.prototype.onClick = function (event) { 2 'use strict'; 3 var permitted; 4 alert('终于尼玛进来了'); 5 if (this.trackingClick) { 6 this.targetElement = null; 7 this.trackingClick = false; 8 return true; 9 } 10 if (event.target.type === 'submit' && event.detail === 0) { 11 return true; 12 } 13 permitted = this.onMouse(event); 14 if (!permitted) { 15 this.targetElement = null; 16 } 17 return permitted; 18 };
而后咱们终于进来了,如今咱们须要知道什么是trackingClick 了
1 /** 2 * Whether a click is currently being tracked. 3 * @type boolean 4 */ 5 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; };
其中用到了一个方法:
1 FastClick.prototype.getTargetElementFromEventTarget = function (eventTarget) { 2 'use strict'; 3 if (eventTarget.nodeType === Node.TEXT_NODE) { 4 return eventTarget.parentNode; 5 } 6 return eventTarget; 7 };
他是获取咱们当前touchstart的元素
而后将鼠标的信息记录了下来,他记录鼠标信息主要在后面touchend时候根据x、y判断是否为click
是ios状况下还搞了一些事情,我这里跳过去了
而后这里记录了一些事情就跳出去了,没有特别的事情,如今咱们进入咱们的出口touchend
FastClick.prototype.onTouchEnd = function (event) { 'use strict'; var forElement, trackingClickStart, targetTagName, scrollParent, touch, 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那些东西如今也执行了......多是我屏幕有变化(滑动)致使
1 if ((event.timeStamp - this.lastClickTime) < 200) { 2 this.cancelNextClick = true; 3 return true; 4 }
这个代码很关键,咱们首次点击会执行下面的逻辑,若是连续点击就直接完蛋,下面的逻辑丫的不执行了......
这个不执行了,那么这个劳什子又干了什么事情呢?
事实上下面就没逻辑了,意思是若是确实点击过快,两次点击只会执行一次,这个阀值为200ms,这个暂时看来是没有问题的
好了,咱们继续往下走,因而我意识到又到了一个关键点
由于咱们用tap事件不能使input得到焦点,可是fastclick却能得到焦点,这里也许是一个关键,咱们来看看几个与获取焦点有关的函数
1 FastClick.prototype.focus = function (targetElement) { 2 'use strict'; 3 var length; 4 if (this.deviceIsIOS && targetElement.setSelectionRange) { 5 length = targetElement.value.length; 6 targetElement.setSelectionRange(length, length); 7 } else { 8 targetElement.focus(); 9 } 10 };
setSelectionRange是咱们的关键,也许他是这样获取焦点的......具体我还要下来测试,留待下次处理吧
而后下面若是时间间隔过长,代码就不认为操做的是同一dom结构了
最后迎来了本次的关键:sendClick,不管是touchend仍是onMouse都会汇聚到这里
1 FastClick.prototype.sendClick = function (targetElement, event) { 2 'use strict'; 3 var clickEvent, touch; 4 // On some Android devices activeElement needs to be blurred otherwise the synthetic click will have no effect (#24) 5 if (document.activeElement && document.activeElement !== targetElement) { 6 document.activeElement.blur(); 7 } 8 touch = event.changedTouches[0]; 9 // Synthesise a click event, with an extra attribute so it can be tracked 10 clickEvent = document.createEvent('MouseEvents'); 11 clickEvent.initMouseEvent('click', true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null); 12 clickEvent.forwardedTouchEvent = true; 13 targetElement.dispatchEvent(clickEvent); 14 };
他建立了一个鼠标事件,而后dispatchEvent事件(这个与fireEvent相似)
//document上绑定自定义事件ondataavailable document.addEventListener('ondataavailable', function (event) { alert(event.eventType); }, false); var obj = document.getElementById("obj"); //obj元素上绑定click事件 obj.addEventListener('click', function (event) { alert(event.eventType); }, false); //调用document对象的 createEvent 方法获得一个event的对象实例。 var event = document.createEvent('HTMLEvents'); // initEvent接受3个参数: // 事件类型,是否冒泡,是否阻止浏览器的默认行为 event.initEvent("ondataavailable", true, true); event.eventType = 'message'; //触发document上绑定的自定义事件ondataavailable document.dispatchEvent(event); var event1 = document.createEvent('HTMLEvents'); event1.initEvent("click", true, true); event1.eventType = 'message'; //触发obj元素上绑定click事件 document.getElementById("test").onclick = function () { obj.dispatchEvent(event1); };
至此,咱们就知道了,咱们为dom先绑定了鼠标事件,而后touchend时候触发了,而至于为何自己注册的click未触发就要回到上面代码了
解决“点透”(成果)
有了这个思路,咱们来试试咱们抽象出来的代码:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title></title> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <style> #list { display: block; position: absolute; top: 100px; left: 10px; width: 200px; height: 100px; } div { display: block; border: 1px solid black; height: 300px; width: 100%; } #input { width: 80px; height: 200px; display: block; } </style> </head> <body> <div id="list" style="background: gray;"> </div> <div id="wrapper"> <div id="d"> <input type="text" id="input" /> </div> </div> <script type="text/javascript"> var el = null; function getEvent(el, e, type) { e = e.changedTouches[0]; var event = document.createEvent('MouseEvents'); event.initMouseEvent(type, true, true, window, 1, e.screenX, e.screenY, e.clientX, e.clientY, false, false, false, false, 0, null); event.forwardedTouchEvent = true; return event; } list.addEventListener('touchstart', function (e) { var firstTouch = e.touches[0] el = firstTouch.target; t1 = e.timeStamp; }) list.addEventListener('touchend', function (e) { e.preventDefault(); var event = getEvent(el, e, 'click'); el.dispatchEvent(event); }) var list = document.getElementById('list'); list.addEventListener('click', function (e) { list.style.display = 'none'; setTimeout(function () { list.style.display = ''; }, 1000); }) </script> </body> </html>
这样的话,便不会点透了,这是由于zepto touch事件所有绑定值document,因此 e.preventDefault();无用
结果咱们这里是直接在dom上,e.preventDefault();
便起了做用不会触发浏览器默认事件,因此也不存在点透问题了,至此点透事件告一段落......
代码在公司写的,回家后不知道图上哪里了,各位将就看吧
我最开始就给老大说zepto处理tap事件不够好,搞了不少事情出来
由于他事件是绑定到document上,先touchstart而后touchend,根据touchstart的event参数判断该dom是否注册了tap事件,有就触发
因而问题来了,zepto的touchend这里有个event参数,咱们event.preventDefault(),这里原本都是最上层了,这就代码压根没什么用
可是fastclick处理办法不可谓不巧妙,这个库直接在touchend的时候就触发了dom上的click事件而替换了原本的触发时间
意思是原来要350-400ms执行的代码忽然就移到了50-100ms,而后这里虽然使用了touch事件可是touch事件是绑定到了具体dom而不是document上
因此e.preventDefault是有效的,咱们能够阻止冒泡,也能够阻止浏览器默认事件,这个才是fastclick的精华部分,不可谓不高啊!!!
整个fastclick代码读来醍醐灌顶,今天收获很大,在此记录
上面的说法有点问题,这修正一下:
首先,咱们回到原来的zepto方案,看看他有什么问题:
由于js标准本不支持tap事件,因此zepto tap是touchstart与touchend模拟而出
zepto在初始化时便给document绑定touch事件,在咱们点击时根据event参数得到当前元素,并会保存点下和离开时候的鼠标位置
根据当前元素鼠标移动范围判断是否为类点击事件,若是是便触发已经注册好的tap事件
而后fastclick处理比较与zepto基本一致,可是又有所不一样
fastclick是将事件绑定到你传的元素(通常是document.body)
② 在touchstart和touchend后(会手动获取当前点击el),若是是类click事件便手动触发了dom元素的click事件
因此click事件在touchend便被触发,整个响应速度就起来了,触发实际与zepto tap同样
好了,为何基本相同的代码,zepto会点透而fastclick不会呢?
缘由是zepto的代码里面有个settimeout,而就算在这个代码里面执行e.preventDefault()也不会有用
这就是根本区别,由于settimeout会将优先级较低
有了按期器,当代码执行到setTimeout的时候, 就会把这个代码放到JS的引擎的最后面
而咱们代码会立刻检测到e.preventDefault,一旦加入settimeout,e.preventDefault便不会生效,这是zepto点透的根本缘由
虽然,此次走了不少弯路,可是最后终于解决了问题