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