這要追溯至 2007 年初。蘋果公司在發佈首款 iPhone 前夕,遇到一個問題 —— 當時的網站都是爲大屏幕設備所設計的。因而蘋果的工程師們作了一些約定,應對 iPhone 這種小屏幕瀏覽桌面端站點的問題。這當中最出名的,當屬雙擊縮放(double tap to zoom)。這也是會有上述 300 毫秒延遲的主要緣由。javascript
當用戶一次點擊屏幕以後,瀏覽器並不能馬上判斷用戶是要進行雙擊縮放,仍是想要進行單擊操做。所以,iOS Safari 就等待 300 毫秒,以判斷用戶是否再次點擊了屏幕。java
因而,300 毫秒延遲就這麼誕生了。git
FastClick 是 FT Labs 專門爲解決移動端瀏覽器 300 毫秒點擊延遲問題所開發的一個輕量級的庫。簡而言之,FastClick 在檢測到 touchend
事件的時候,會經過 DOM 自定義事件當即觸發一個模擬click
事件的click事件(自定義事件),並把瀏覽器在 300 毫秒以後真正觸發的 click
事件阻止掉。github
FastClick 很是實際地解決 300 毫秒點擊延遲的問題。惟一的缺點可能也就是該腳本的文件尺寸 (儘管它不大)。若是你很是在乎這點文件大小,能夠嘗試一下 Filament Group 的 Tappy!,或者 tap.js。二者都至關輕量,可以經過監聽 tap
而非 click
事件來繞過 300 毫秒延遲。瀏覽器
固然,zepto庫函數中,也有一個touch模塊,此模塊也包含tap事件,能夠經過綁定tap來取代click事件。app
可是zepto的tap事件會有點透問題。如何解決,請看下篇分解。ide
接下來,咱們來詳細瞭解一個問題:FastClick解決延遲點擊的源碼解析。函數
首先,咱們來看FastClick的使用。網站
window.addEventListener( "load", function() { FastClick.attach( document.body ); }, false );
這樣就解決了 300 毫秒點擊延遲的問題。this
FastClick的源碼:
FastClick.attach = function(layer) { 'use strict'; return new FastClick(layer); };
在FastClick的構造函數中,會有下面這段代碼:
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.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('touchend', this.onTouchEnd, false); layer.addEventListener('touchcancel', this.onTouchCancel, false);
也就是在document.body上綁定了click,touchstart,touchend,touchcancel事件。
這裏假設,咱們的頁面有一個button,綁定了click事件。
當用戶點擊此button時,會先觸發touchstart事件,這時,會冒泡到document.body中,因而就會執行:
FastClick.prototype.onTouchStart = function(event) { 'use strict'; var targetElement, touch, selection; 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; };
這個回調函數主要作了如下事情:
獲取咱們當前觸發touchstart的元素,這裏是button。
而後將鼠標的信息記錄了下來,記錄鼠標的信息主要是爲了在後面touchend觸發時,根據這裏獲得的x、y判斷是否爲click。
以後,會觸發touchend事件,而後冒泡到document.body上,執行如下代碼:
FastClick.prototype.onTouchEnd = function(event) { 'use strict'; var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement; if (this.touchHasMoved(event) || (event.timeStamp - this.trackingClickStart) > 300) { this.trackingClick = false; this.targetElement = null; } 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); } 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; };
注意上面的代碼中,event.preventDefault();會阻止真實的click事件的觸發,所以,在button上面的click事件不會觸發。
接下來,咱們只須要查看sendClick方法。
FastClick.prototype.sendClick = function(targetElement, event) { 'use strict'; var clickEvent, touch; if (document.activeElement && document.activeElement !== targetElement) { document.activeElement.blur(); } touch = event.changedTouches[0]; 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); };
在此方法中,會建立一個自定義的click事件,而後在button上當即觸發,因而,button綁定的click的事件回調函數立刻執行,所以就沒有300ms延遲了。
上面的initMouseEvent方法的前三個參數的意思:1.事件類型,2.是否冒泡,3.是否阻止瀏覽器的默認行爲。
自定義的click事件阻止了瀏覽器的默認行爲,事件冒泡,因而執行document.body的click事件回調函數。代碼以下:
FastClick.prototype.onClick = function(event) { 'use strict'; var permitted; 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; };
而後裏面有一句 permitted = this.onMouse(event);因而,咱們查看onMouse方法:
FastClick.prototype.onMouse = function(event) { 'use strict'; if (!this.targetElement) { return true; } if (event.forwardedTouchEvent) { return true; } if (!event.cancelable) { return true; } if (!this.needsClick(this.targetElement) || this.cancelNextClick) { if (event.stopImmediatePropagation) { event.stopImmediatePropagation(); } else { event.propagationStopped = true; } event.stopPropagation(); event.preventDefault(); return false; } return true; };
此方法,會阻止模擬的click事件的冒泡以及默認行爲。
到此,解決了移動端瀏覽器click事件延遲300ms的問題。
加油!