FastClick源码分析
玩過移動端web開發的同學應該都了解過,移動端上的click事件都會有300毫秒的延遲,這300毫秒主要是瀏覽器為了判斷你當前的點擊時單擊還是雙擊,但有時候為了更快的對用戶的操作做出更快的響應,越過這個300毫秒的延遲是有點必要的,FastClick做的就是這件事,這篇文章會理清FastClick的整體思路,分析主要的代碼,但不會貼出所有的代碼,僅分析主干,由于歷史原因,FastClick對舊版本的機型做了很多兼容性適配,例如ios4,這部分代碼到現在顯然已經沒有什么分析的意義了,所以貼出的代碼會將這部分代碼刪除。
首先,我們分析一下總體的實現思路,其實FastClick做的事情很簡單,首先判斷當前瀏覽器需不需要使用FastClick,例如桌面瀏覽器,那就不需要,直接繞過,接著,如果需要,則在click事件中攔截事件,取消所有綁定事件的操作,接著用一系列touch事件(touchstart,touchmove,touchend)來模擬click事件,由于touch事件不會延遲,從而達到繞過300毫秒延遲的效果。
先看看FastClick是如何判斷瀏覽器是否需要FastClick的
FastClick.notNeeded = function(layer) {var metaViewport;var chromeVersion;var blackberryVersion;var firefoxVersion;// Devices that don't support touch don't need FastClick//不支持用于模擬的touchstart事件,無法模擬if (typeof window.ontouchstart === 'undefined') {return true;}// 探測chome瀏覽器chromeVersion = +(/Chrome\/([0-9]+)/.exec(navigator.userAgent) || [,0])[1];if (chromeVersion) {//安卓設備if (deviceIsAndroid) {metaViewport = document.querySelector('meta[name=viewport]');if (metaViewport) {// 安卓下,帶有 user-scalable="no" 的 meta 標簽的 chrome 是會自動禁用 300ms 延遲的,無需 FastClickif (metaViewport.content.indexOf('user-scalable=no') !== -1) {return true;}//chome32以上帶有 width=device-width的meta標簽的也唔需要使用FastClickif (chromeVersion > 31 && document.documentElement.scrollWidth <= window.outerWidth) {return true;}}// 桌面設備自然無需使用} else {return true;}}//黑莓瀏覽器,這個。。。了解就好if (deviceIsBlackBerry10) {//檢測黑莓瀏覽器blackberryVersion = navigator.userAgent.match(/Version\/([0-9]*)\.([0-9]*)/);// 黑莓10.3以上部分可以不適用FastClickif (blackberryVersion[1] >= 10 && blackberryVersion[2] >= 3) {metaViewport = document.querySelector('meta[name=viewport]');if (metaViewport) {// 跟chome一樣if (metaViewport.content.indexOf('user-scalable=no') !== -1) {return true;}// 跟chome一樣if (document.documentElement.scrollWidth <= window.outerWidth) {return true;}}}}//ie10帶有msTouchAction,touchAction相關樣式的不需要FastClickif (layer.style.msTouchAction === 'none' || layer.style.touchAction === 'manipulation') {return true;}//firefox,跟chome差不多firefoxVersion = +(/Firefox\/([0-9]+)/.exec(navigator.userAgent) || [,0])[1];if (firefoxVersion >= 27) {// Firefox 27+ does not have tap delay if the content is not zoomable - https://bugzilla.mozilla.org/show_bug.cgi?id=922896metaViewport = document.querySelector('meta[name=viewport]');if (metaViewport && (metaViewport.content.indexOf('user-scalable=no') !== -1 || document.documentElement.scrollWidth <= window.outerWidth)) {return true;}}//ie11檢測,跟ie10一樣,只是ie11廢棄了msTouchAction,改為touchAction,依舊是檢測樣式,檢測到相關樣式不用FastClickif (layer.style.touchAction === 'none' || layer.style.touchAction === 'manipulation') {return true;}//黑名單之外放行,都使用FastClickreturn false;};長長的一大段,基本上采用黑名單策略,分別檢測了chome,黑莓,firefox,ie10,ie11,基本上都是檢測對應的meta標簽,檢測到對應的值的話,棄用FastClick,黑名單之外啟用FastClick,僅僅是一個檢測函數,看看就好,沒什么研究的價值
主體流程,看看FastClick的構造函數,此處僅貼出主要代碼,刪除了一些兼容的代碼
function FastClick(layer, options) {//不需要fastClick時直接返回if (FastClick.notNeeded(layer)) {return;}//簡單的兼容bind方法function bind(method, context) {return function() { return method.apply(context, arguments); };}//注冊內部事件var methods = ['onMouse', 'onClick', 'onTouchStart', 'onTouchMove', 'onTouchEnd', 'onTouchCancel'];var context = this;for (var i = 0, l = methods.length; i < l; i++) {context[methods[i]] = bind(context[methods[i]], context);}//捕獲階段做攔截事件處理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);//處理通過標簽屬性綁定事件的方式,轉化為通過addEventListener綁定事件,確保fastclick的各種兼容能順利執行if (typeof layer.onclick === 'function') {oldOnClick = layer.onclick;layer.addEventListener('click', function(event) {oldOnClick(event);}, false);layer.onclick = null;}}FastClick會在執行FastClick.attach操作時被實例化,從代碼我們可以看到,做了幾件事,檢測是否需要使用FastClick,之后注冊了一些列的內部方法(onmouse,onclik,ontouchstart等等)并綁定當前作用域,捕獲階段處理onclick事件,冒泡階段處理touch相關事件并定義相關的內部處理函數,最后對于用標簽綁定事件的方式修改為用addEventListener的方式綁定。至于為什么為什么要在捕獲階段處理onclick,我們都知道,現代瀏覽器對于事件的處理都是先發生捕獲,之后再發生冒泡,而為了兼容舊版本瀏覽器,默認的做法都是將事件綁定在冒泡階段,在冒泡階段處理click事件,我們就可以攔截到click事件,并把后續的click綁定操作全都取消掉。
所以,我們大概可以看到,FastClick里面最主要的幾個主要方法:onMouse,onClick,onTouchStart,onTouchMoce,onTouchEnd,onTouchMove,onTouchCancel,接下來我們將會逐個分析這些方法
首先,onClick方法
FastClick.prototype.onClick = function(event) {var permitted;// 標記未被取消,直接取消if (this.trackingClick) {this.targetElement = null;this.trackingClick = false;return true;}//submit控件不做處理if (event.target.type === 'submit' && event.detail === 0) {return true;}permitted = this.onMouse(event);if (!permitted) {this.targetElement = null;}return permitted;};此處有必要解釋一下trackingClick和targetElement這兩個標記,trackingClick是一個追蹤標志,用touch事件模擬時,正常情況下,開始時(touchstart)會被設置為true,模擬結束(touchend)會被設置為false,而click事件會在touchend事件中被模擬發出,這個后面分析代碼的時候我們會看到,很明顯,這個時候trackingClick如果檢測到為true,是一種不正常的現象,這里FastClick的作者解釋為you可能使用了類似的第三方庫,導致click事件比FastClick更快的發出,所以此處就不再對結果進行處理,并將內部變量重現修改為默認狀態。接著,我們看到,onclick方法其實在內部調用了onmouse方法,事實上主要的操作也都是在onmouse里面執行的,接下來我們看看onMouse
FastClick.prototype.onMouse = function(event) {//當前target缺失,有可能模擬觸發已經被取消,沒有必要阻止 ,直接觸發原生事件if (!this.targetElement) {return true;}//模擬事件標識符if (event.forwardedTouchEvent) {return true;}// 事件無法阻止if (!event.cancelable) {return true;}//需要fastclick是阻止所有事件觸發,快速點擊時亦如此if (!this.needsClick(this.targetElement) || this.cancelNextClick) {// Prevent any user-added listeners declared on FastClick element from being fired.//解除所有后續事件的觸發,包括當前節點綁定的其他事件if (event.stopImmediatePropagation) {event.stopImmediatePropagation();} else {// Part of the hack for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2)event.propagationStopped = true;}// 阻止冒泡,阻止默認操作event.stopPropagation();event.preventDefault();return false;}// If the mouse event is permitted, return true for the action to go through.return true;};首先,進入onMouse之后,會通過函數needClick判斷當前點擊的控件是否需要原生點擊的支持,避免出現一些bug,然后判斷this.cancelNextClick是否為true,cancelNextClick是用于判斷當前操作是否要取消的一個標識符,當兩次點擊的間隔小于配置的值時,cancelNextClick會被設置為true,這個操作在touchend中進行,稍后會進行分析。當條件滿足時,執行阻止事件的操作,具體是執行event.stopImmediatePropagation方法,他能阻止此操作之后綁定在這個節點上的所有其他操作,對于不支持的瀏覽器,會在event中添加一個propagationStopped的屬性,用于兼容操作,這個兼容操作后面再說,接著就是各種阻止冒泡,阻止默認操作,至此,整個阻止操作就完成了,接下來就是如何不延遲300毫秒來觸發click事件了,上面說了,用touch事件進行模擬,具體如何,往下走
首先,onTouchStart
FastClick.prototype.onTouchStart = function(event) {var targetElement, touch, selection;//忽略多點觸控if (event.targetTouches.length > 1) {return true;}targetElement = this.getTargetElementFromEventTarget(event.target);touch = event.targetTouches[0];//記錄跟蹤狀態this.trackingClick = true;//記錄開始點擊時間this.trackingClickStart = event.timeStamp;//記錄當前處理的節點this.targetElement = targetElement;//記錄當前位置this.touchStartX = touch.pageX;this.touchStartY = touch.pageY;// Prevent phantom clicks on fast double-tap (issue #36)//阻止雙擊事件的默認動作if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {event.preventDefault();}return true;};onTouchStart做的事情其實比較少,上面的代碼去掉了一些兼容性操作,剩下的只是記錄一些基礎性的信息,唯一做的事情就是阻止了雙擊事件的默認操作,如何判斷是雙擊的,event.timeStamp記錄了當前點擊的時間戳,this.lastClickTime為上一次onTouchEnd時記錄的值,記錄最后一次點擊完成的時間,兩者相減小于配置值,則認為是雙擊,FastClick默認配置的this.tapDelay為200毫秒
接著是onTouchMove
FastClick.prototype.onTouchMove = function(event) {//沒有觸發過touchstart事件,直接返回if (!this.trackingClick) {return true;}// If the touch has moved, cancel the click tracking//判斷當前是否移動,移動過則取消跟蹤事件if (this.targetElement !== this.getTargetElementFromEventTarget(event.target) || this.touchHasMoved(event)) {this.trackingClick = false;this.targetElement = null;}return true;};操作也是比較簡單,trackingClick是一個跟蹤字段,在onTouchStart中設置為true,如此處發現不為true,則發生了錯誤,直接會返回,接著就是判斷當前是否有移動,主要就是獲取當前手指的位置跟觸發控件的位置進行比較,具體方法由于篇幅關系就不解釋了,本篇博文僅解釋主干內容,當觸摸點移動了,則將trackingClcik和targetElement恢復為默認,之后在touchEnd中就不會發出模擬事件觸發click
接著對于特殊原因取消的情況,綁定了touchcancel事件
FastClick.prototype.onTouchCancel = function() {this.trackingClick = false;this.targetElement = null;};這個并沒有什么特別的地方,特殊情況發生了,如手指戳下的時候突然來電話了各種情況導致觸摸中斷,則將所有跟蹤變量恢復到初始狀態。
最關鍵的onTouchEnd
FastClick.prototype.onTouchEnd = function(event) {var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement;//觸摸點移動或者其他操作導致取消if (!this.trackingClick) {return true;}//不處理快速點擊if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {this.cancelNextClick = true;return true;}//不處理長按if ((event.timeStamp - this.trackingClickStart) > this.tapTimeout) {return true;}// 將所有的跟蹤變量設置為初始狀態,供下次點擊使用this.cancelNextClick = false;this.lastClickTime = event.timeStamp;trackingClickStart = this.trackingClickStart;this.trackingClick = false;this.trackingClickStart = 0;targetTagName = targetElement.tagName.toLowerCase();//處理組件為label時的狀況,獲取label對應綁定的控件if (targetTagName === 'label') {forElement = this.findControl(targetElement);if (forElement) {this.focus(targetElement);if (deviceIsAndroid) {return false;}targetElement = forElement;}} else if (this.needsFocus(targetElement)) {//第一個判斷作者認為如果按下的時間超過了100毫秒,此時已經沒有必要再執行模擬操作了,按原生的click執行操作即可,第二個判斷則是處理ios相關的一個bugif ((event.timeStamp - trackingClickStart) > 100 || (deviceIsIOS && window.top !== window && targetTagName === 'input')) {this.targetElement = null;return false;}this.focus(targetElement);this.sendClick(targetElement, event);return false;}//不需要原生點擊時,觸發模擬click事件if (!this.needsClick(targetElement)) {event.preventDefault();this.sendClick(targetElement, event);}return false;};此處,ontouchEnd,首先忽略快速點擊和長按,然后恢復所有的初始化變量,之后會判斷當前控件是不是label,是的話利用findControl函數找到label關聯的組件,并賦值給當前的targetElement 統一處理,具體雜七雜八的函數會在后面再解釋,接著會判斷當前組件觸發click時需不需要獲取焦點,如果需要,則獲取焦點后,觸發模擬事件,此處關注兩個函數focus和sendClick,focus函數幫助當前target獲取焦點,sendClick則發送模擬事件,focus函數關鍵代碼如下
/*** 兼容寫法,獲取焦點,光標放置到末尾*/FastClick.prototype.focus = function(targetElement) {var length;if (deviceIsIOS && targetElement.setSelectionRange && targetElement.type.indexOf('date') !== 0 && targetElement.type !== 'time' && targetElement.type !== 'month') {length = targetElement.value.length;targetElement.setSelectionRange(length, length);} else {targetElement.focus();}};此處,對于ios瀏覽器,采用兼容的寫法,用setSelectionRange來獲取焦點,setSelectionRange可以用來選取輸入框的值,此處將選取的開始和結束都設置為value的length,則可以把光標放到組件的末尾并且獲得焦點
接下來是sendClick,這也是整個fastclick的關鍵,用于模擬事件的發生,主要實現如下:
FastClick.prototype.sendClick = function(targetElement, event) {var clickEvent, touch;//兼容操作,部分安卓機當前焦點所在的節點如果不是模擬節點,需要把焦點去除,否則影響效果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 trackedclickEvent = document.createEvent('MouseEvents');clickEvent.initMouseEvent(this.determineEventType(targetElement), true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);clickEvent.forwardedTouchEvent = true;targetElement.dispatchEvent(clickEvent);}; 實現代碼很簡單,就是就是創建一個event對象,然后觸發它,注意,這個地方用到了initMouseEvent來初始化event對象,但目前initMouseEvent已經從web刪除了,換句話說它已經不是標準方法了,未來的瀏覽器可能不會再繼續提供支持,所以自己盡量不要使用這個特性,可以用MouseEvent這個特定的事件構造器來替代它,詳細使用方法可以參考戳我帶你飛至此,我們的所有主流程已經講完了,接下來我們說一下里面涉及到的一些雜七雜八的函數
首先,如何兼容event.stopImmediatePropagation,上面我們說了,這個函數可以解除當前綁定操作之后的所有綁定到此節點上的操作,但存在部分瀏覽器不兼容,對于一些不兼容的瀏覽器,上面說到綁定事件fastclick會手動給event對象添加一個propagationStopped屬性,那這個屬性有什么用呢,我們看看下面的代碼
layer.addEventListener = function(type, callback, capture) {var adv = Node.prototype.addEventListener;if (type === 'click') {adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) {//通過對event對象添加屬性來控制事件的觸發if (!event.propagationStopped) {callback(event);}}), capture);} else {adv.call(layer, type, callback, capture);}};這段函數出現在fastclick的構造函數中,為了主干代碼的清晰,在上面我把它刪掉了,對于不兼容event.stopImmediatePropagation的瀏覽器,它重寫了addEventListener方法,增加了對stopImmediatePropagation屬性的判斷,這樣當上面的propagationStopped被設置為true的時候,后續的綁定操作就都不會繼續進行了。
接下來一個方法是獲取label關聯控件的方法,findControl
FastClick.prototype.findControl = function(labelElement) {//通過control屬性獲取if (labelElement.control !== undefined) {return labelElement.control;}//通過獲取for屬性if (labelElement.htmlFor) {return document.getElementById(labelElement.htmlFor);}//如各種不兼容,則獲取label標簽中的第一個return labelElement.querySelector('button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea');};首先,findControl會通過html5的control屬性來獲取label包含的表單元素,如果失敗,轉而獲取label的for屬性對應的表單元素,因為for屬性也是html5的,舊瀏覽器可能不兼容,最后如果獲取不了,則會獲取label元素的子元素中的第一個表單元素,進而來獲取label對應的表單元素。
嗯,啰啰嗦嗦大概說完了,如有說錯的地方,歡迎評論區指出
總結
以上是生活随笔為你收集整理的FastClick源码分析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 支付服务器维护费怎么做账,税控盘维护费的
- 下一篇: OTB 数据集的跟踪结果