您好,登錄后才能下訂單哦!
這篇文章主要為大家展示了“如何通過fastclick源碼分析徹底解決tap“點透””,內容簡而易懂,條理清晰,希望能夠幫助大家解決疑惑,下面讓小編帶領大家一起研究并學習一下“如何通過fastclick源碼分析徹底解決tap“點透””這篇文章吧。
讀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('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; } }
看看這段代碼,上面很多屬性干了什么事情我也不知道......于是忽略了
if (!layer || !layer.nodeType) { throw new TypeError('Layer must be a document node'); }
其中這里要注意,我們必須傳入一個節點給構造函數,否則會出問題
然后這個家伙將一些基本的鼠標事件注冊在自己的屬性方法上了,具體是干神馬的我們后面再說
在后面點有個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,注釋的意思不太明白,我們看看代碼吧
首先一句:
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('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
完了多了一個屬性:
阻止當前事件的冒泡行為并且阻止當前事件所在元素上的所有相同類型事件的事件處理函數的繼續執行.
如果某個元素有多個相同類型事件的事件監聽函數,則當該類型的事件觸發時,多個事件監聽函數將按照順序依次執行.如果某個監聽函數執行了 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進行了特殊處理
adv.call(layer, type, 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; }
此處,他的主干流程居然就完了,意思是他所有的邏輯就在這里了,不論入口還是出口應該就是事件注冊了,于是我們寫個代碼來看看
測試入口
<input type="button" value="addevent"> <input type="button" value="addevent1"> $('#addEvent').click(function () { var dom = $('#addEvent1')[0] dom.addEventListener('click', function () { alert('') var s = ''; }) });
我們來這個斷點看看我們點擊后干了什么,我們現在點擊按鈕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, 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那些東西現在也執行了......可能是我屏幕有變化(滑動)導致
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, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null); clickEvent.forwardedTouchEvent = true; targetElement.dispatchEvent(clickEvent); };
他創建了一個鼠標事件,然后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> </div> <div> <div> <input type="text" /> </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會點透/fastclick如何解決點透
我最開始就給老大說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點透的根本原因
以上是“如何通過fastclick源碼分析徹底解決tap“點透””這篇文章的所有內容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。