閃爍問題
由于在iOS Safari上click事件存在300ms響應延時,所以為touch事件添加樣式,會和click事件默認樣式疊加而產生閃爍問題。
因為ios safari瀏覽器中對觸摸事件的響應順序是:
ontouchstart -> ontouchmove -> ontouchend -> (300ms) onclick
快速解決問題的方法:
消除click默認樣式
通過講click事件樣式的alpha通道設置為0(透明),消除click事件的閃爍效果
-webkit-tap-highlight-color: rgba(0,0,0,0);
此時,瀏覽器默認的click事件樣式就看不到了,但是延時仍然存在。
消除click延時
FastClick 是FT Labs專門為解決移動端瀏覽器 300 毫秒點擊延遲問題所開發的一個輕量級的庫。簡而言之,FastClick 在檢測到 touchend 事件的時候,會通過 DOM 自定義事件立即觸發一個模擬 click 事件,并把瀏覽器在 300 毫秒之后真正觸發的click事件阻止掉。
FastClick 非常實際地解決 300 毫秒點擊延遲的問題,唯一的缺點可能也就是該腳本的文件尺寸 (盡管它只有10kb)。如果你連這10kb都接受不了的話,那么移動端類庫 jQuery和zepto.js都支持tap事件來解決這個問題,盡管它們的響應速度比FastClick慢一些。
延伸閱讀
iOS端click事件響應延時(300ms)的由來
這要追溯至 2007 年初。蘋果公司在發布首款 iPhone 前夕,遇到一個問題:當時的網站都是為大屏幕設備所設計的。于是蘋果的工程師們做了一些約定,應對 iPhone 這種小屏幕瀏覽桌面端站點的問題。 這當中最出名的,當屬雙擊縮放(double tap to zoom),這也是會有上述 300 毫秒延遲的主要原因。雙擊縮放,顧名思義,即用手指在屏幕上快速點擊兩次,iOS 自帶的 Safari 瀏覽器會將網頁縮放至原始比例。
那么這和 300 毫秒延遲有什么聯系呢? 假定這么一個場景。用戶在 iOS Safari 里邊點擊了一個鏈接。由于用戶可以進行雙擊縮放或者雙擊滾動的操作,當用戶一次點擊屏幕之后,瀏覽器并不能立刻判斷用戶是確實要打開這個鏈接,還是想要進行雙擊操作。因此,iOS Safari 就等待 300 毫秒,以判斷用戶是否再次點擊了屏幕。 鑒于iPhone的成功,其他移動瀏覽器都復制了 iPhone Safari 瀏覽器的多數約定,包括雙擊縮放,幾乎現在所有的移動端瀏覽器都有這個功能。
之前人們剛剛接觸移動端的頁面,在欣喜的時候往往不會care這個300ms的延時問題,可是如今touch端界面如雨后春筍,用戶對體驗的要求也更高,這300ms帶來的卡頓慢慢變得讓人難以接受。
FastClick解決延遲點擊的源碼解析。
首先,我們來看FastClick的使用。
window.addEventListener( "load", function() {
FastClick.attach( document.body );
}, false );
這樣就解決了 300 毫秒點擊延遲的問題。
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的問題。
<i>本文大部分內容來自互聯網</i>