移動端click事件延遲300ms的緣由以及解決辦法

這要追溯至 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解決延遲點擊的源碼解析。

首先,咱們來看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的問題。

  

  

  

 

 

加油!  

相關文章
相關標籤/搜索