FastClick 填坑及源码解析
最近產(chǎn)品妹子提出了一個(gè)體驗(yàn)issue —— 用 iOS 在手Q閱讀書友交流區(qū)發(fā)表書評(píng)時(shí),光標(biāo)點(diǎn)擊總是不好定位到正確的位置:
如上圖,具體表現(xiàn)是較快點(diǎn)擊時(shí),光標(biāo)總會(huì)跳到 textarea 內(nèi)容的尾部。只有當(dāng)點(diǎn)擊停留時(shí)間較久一點(diǎn)(比如超過150ms)才能把光標(biāo)正常定位到正確的位置。
一開始我以為是 iOS 原生的交互問題沒太在意,但后來發(fā)現(xiàn)訪問某些頁(yè)面又是沒有這種奇怪體驗(yàn)的。
然后懷疑是否 JS 注冊(cè)了某些事件導(dǎo)致的問題,于是試著把業(yè)務(wù)模塊移除了再跑一遍,發(fā)現(xiàn)問題照舊。
于是只好繼續(xù)做排除法,把頁(yè)面上的一些庫(kù)一點(diǎn)點(diǎn)移掉再運(yùn)行頁(yè)面,結(jié)果發(fā)現(xiàn)搗亂的小鬼果然是嫌疑最大的 Fastclick。
然后呢,我試著按API所說,給 textarea 加上一個(gè)名為“needsclick”的類名,希望能繞過 fastclick 的處理直接走原生點(diǎn)擊事件,結(jié)果訝異地發(fā)現(xiàn)屁用沒有。。。
對(duì)此感謝后面我們小組的 kindeng 童鞋幫忙研究了下并提供了解決方案,不過我還想進(jìn)一步研究到底是什么原因?qū)е铝诉@個(gè)坑、Fastclick 對(duì)我的頁(yè)面做了神馬~
所以昨晚花了點(diǎn)時(shí)間一口氣把源碼都蹂躪了一遍。
這會(huì)是一篇很長(zhǎng)的文章,但會(huì)是注釋非常詳盡的剖析文。
文章帶分析的源碼我也掛在我的 github 倉(cāng)庫(kù)上了,有興趣的童鞋可以去下載來看。
閑話不多說,咱們開始深入 FastClick?源碼陣營(yíng)。
我們知道,注冊(cè)一個(gè) FastClick?事件非常簡(jiǎn)單,它是這樣的:
if ('addEventListener' in document) {document.addEventListener('DOMContentLoaded', function() {var fc = FastClick.attach(document.body); //生成實(shí)例}, false); }所以我們從這里著手,打開源碼看下 FastClick .attach 方法:
FastClick.attach = function(layer, options) {return new FastClick(layer, options);};這里返回了一個(gè)?FastClick 實(shí)例,所以咱們拉到前面看看?FastClick 構(gòu)造函數(shù):
function FastClick(layer, options) {var oldOnClick;options = options || {};//定義了一些參數(shù)...//如果是屬于不需要處理的元素類型,則直接返回if (FastClick.notNeeded(layer)) {return;}//語(yǔ)法糖,兼容一些用不了 Function.prototype.bind 的舊安卓//所以后面不走 layer.addEventListener('click', this.onClick.bind(this), true);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);}//安卓則做額外處理if (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('touchmove', this.onTouchMove, false);layer.addEventListener('touchend', this.onTouchEnd, false);layer.addEventListener('touchcancel', this.onTouchCancel, false);// 兼容不支持 stopImmediatePropagation 的瀏覽器(比如 Android 2)if (!Event.prototype.stopImmediatePropagation) {layer.removeEventListener = function(type, callback, capture) {var rmv = Node.prototype.removeEventListener;if (type === 'click') {rmv.call(layer, type, callback.hijacked || callback, capture);} else {rmv.call(layer, type, callback, capture);}};layer.addEventListener = function(type, callback, capture) {var adv = Node.prototype.addEventListener;if (type === 'click') {//留意這里 callback.hijacked 中會(huì)判斷 event.propagationStopped 是否為真來確保(安卓的onMouse事件)只執(zhí)行一次//在 onMouse 事件里會(huì)給 event.propagationStopped 賦值 trueadv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) {if (!event.propagationStopped) {callback(event);}}), capture);} else {adv.call(layer, type, callback, capture);}};}// 如果layer直接在DOM上寫了 onclick 方法,那我們需要把它替換為 addEventListener 綁定形式if (typeof layer.onclick === 'function') {oldOnClick = layer.onclick;layer.addEventListener('click', function(event) {oldOnClick(event);}, false);layer.onclick = null;}}在初始通過?FastClick.notNeeded 方法判斷是否需要做后續(xù)的相關(guān)處理:
//如果是屬于不需要處理的元素類型,則直接返回if (FastClick.notNeeded(layer)) {return;}我們看下這個(gè)?FastClick.notNeeded 都做了哪些判斷:
//是否沒必要使用到 Fastclick 的檢測(cè)FastClick.notNeeded = function(layer) {var metaViewport;var chromeVersion;var blackberryVersion;var firefoxVersion;// 不支持觸摸的設(shè)備if (typeof window.ontouchstart === 'undefined') {return true;}// 獲取Chrome版本號(hào),若非Chrome則返回0chromeVersion = +(/Chrome\/([0-9]+)/.exec(navigator.userAgent) || [,0])[1];if (chromeVersion) {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;}// 安卓Chrome 32 及以上版本,若帶有 width=device-width 的 meta 標(biāo)簽也是無(wú)需 FastClick 的if (chromeVersion > 31 && document.documentElement.scrollWidth <= window.outerWidth) {return true;}}// 其它的就肯定是桌面級(jí)的 Chrome 了,更不需要 FastClick 啦} else {return true;}}if (deviceIsBlackBerry10) { //黑莓,和上面安卓同理,就不寫注釋了blackberryVersion = navigator.userAgent.match(/Version\/([0-9]*)\.([0-9]*)/);if (blackberryVersion[1] >= 10 && blackberryVersion[2] >= 3) {metaViewport = document.querySelector('meta[name=viewport]');if (metaViewport) {if (metaViewport.content.indexOf('user-scalable=no') !== -1) {return true;}if (document.documentElement.scrollWidth <= window.outerWidth) {return true;}}}}// 帶有 -ms-touch-action: none / manipulation 特性的 IE10 會(huì)禁用雙擊放大,也沒有 300ms 時(shí)延if (layer.style.msTouchAction === 'none' || layer.style.touchAction === 'manipulation') {return true;}// Firefox檢測(cè),同上firefoxVersion = +(/Firefox\/([0-9]+)/.exec(navigator.userAgent) || [,0])[1];if (firefoxVersion >= 27) {metaViewport = document.querySelector('meta[name=viewport]');if (metaViewport && (metaViewport.content.indexOf('user-scalable=no') !== -1 || document.documentElement.scrollWidth <= window.outerWidth)) {return true;}}// IE11 推薦使用沒有“-ms-”前綴的 touch-action 樣式特性名if (layer.style.touchAction === 'none' || layer.style.touchAction === 'manipulation') {return true;}return false;};基本上都是一些能禁用 300ms 時(shí)延的瀏覽器嗅探,它們都沒必要使用 Fastclick,所以會(huì)返回 true 回構(gòu)造函數(shù)停止下一步執(zhí)行。
由于安卓手Q的 ua 會(huì)被匹配到?/Chrome\/([0-9]+)/,故帶有?'user-scalable=no' meta 標(biāo)簽的安卓手Q頁(yè)會(huì)被 FastClick?視為無(wú)需處理頁(yè)。
這也是為何在安卓手Q里沒有開頭提及問題的原因。
我們繼續(xù)看構(gòu)造函數(shù),它直接給 layer(即body)添加了click、touchstart、touchmove、touchend、touchcancel(若是安卓還有 mouseover、mousedown、mouseup)事件監(jiān)聽:
//安卓則做額外處理if (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('touchmove', this.onTouchMove, false);layer.addEventListener('touchend', this.onTouchEnd, false);layer.addEventListener('touchcancel', this.onTouchCancel, false);注意在這段代碼上面還利用了 bind 方法做了處理,這些事件回調(diào)中的 this 都會(huì)變成 Fastclick 實(shí)例上下文。
另外還得留意,onclick 事件以及安卓的額外處理部分都是走的捕獲監(jiān)聽。
咱們分別看看這些事件回調(diào)分別都做了什么。
1. this.onTouchStart
FastClick.prototype.onTouchStart = function(event) {var targetElement, touch, selection;// 多指觸控的手勢(shì)則忽略if (event.targetTouches.length > 1) {return true;}targetElement = this.getTargetElementFromEventTarget(event.target); //一些較老的瀏覽器,target 可能會(huì)是一個(gè)文本節(jié)點(diǎn),得返回其DOM節(jié)點(diǎn)touch = event.targetTouches[0];if (deviceIsIOS) { //IOS處理// 若用戶已經(jīng)選中了一些內(nèi)容(比如選中了一段文本打算復(fù)制),則忽略selection = window.getSelection();if (selection.rangeCount && !selection.isCollapsed) {return true;}if (!deviceIsIOS4) { //是否IOS4//怪異特性處理——若click事件回調(diào)打開了一個(gè)alert/confirm,用戶下一次tap頁(yè)面的其它地方時(shí),新的touchstart和touchend//事件會(huì)擁有同一個(gè)touch.identifier(新的 touch event 會(huì)跟上一次觸發(fā)alert點(diǎn)擊的 touch event 一樣),//為避免將新的event當(dāng)作之前的event導(dǎo)致問題,這里需要禁用事件//另外chrome的開發(fā)工具啟用'Emulate touch events'后,iOS UA下的 identifier 會(huì)變成0,所以要做容錯(cuò)避免調(diào)試過程也被禁用事件了if (touch.identifier && touch.identifier === this.lastTouchIdentifier) {event.preventDefault();return false;}this.lastTouchIdentifier = touch.identifier;// 如果target是一個(gè)滾動(dòng)容器里的一個(gè)子元素(使用了 -webkit-overflow-scrolling: touch) ,而且滿足:// 1) 用戶非常快速地滾動(dòng)外層滾動(dòng)容器// 2) 用戶通過tap停止住了這個(gè)快速滾動(dòng)// 這時(shí)候最后的'touchend'的event.target會(huì)變成用戶最終手指下的那個(gè)元素// 所以當(dāng)快速滾動(dòng)開始的時(shí)候,需要做檢查target是否滾動(dòng)容器的子元素,如果是,做個(gè)標(biāo)記// 在touchend時(shí)檢查這個(gè)標(biāo)記的值(滾動(dòng)容器的scrolltop)是否改變了,如果是則說明頁(yè)面在滾動(dòng)中,需要取消fastclick處理this.updateScrollParent(targetElement);}}this.trackingClick = true; //做個(gè)標(biāo)志表示開始追蹤click事件了this.trackingClickStart = event.timeStamp; //標(biāo)記下touch事件開始的時(shí)間戳this.targetElement = targetElement;//標(biāo)記touch起始點(diǎn)的頁(yè)面偏移值this.touchStartX = touch.pageX;this.touchStartY = touch.pageY;// this.lastClickTime 是在 touchend 里標(biāo)記的事件時(shí)間戳// this.tapDelay 為常量 200 (ms)// 此舉用來避免 phantom 的雙擊(200ms內(nèi)快速點(diǎn)了兩次)觸發(fā) click// 反正200ms內(nèi)的第二次點(diǎn)擊會(huì)禁止觸發(fā)其默認(rèn)事件if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {event.preventDefault();}return true;};順道看下這里的?this.updateScrollParent:
/*** 檢查target是否一個(gè)滾動(dòng)容器里的子元素,如果是則給它加個(gè)標(biāo)記*/FastClick.prototype.updateScrollParent = function(targetElement) {var scrollParent, parentElement;scrollParent = targetElement.fastClickScrollParent; if (!scrollParent || !scrollParent.contains(targetElement)) {parentElement = targetElement;do {if (parentElement.scrollHeight > parentElement.offsetHeight) {scrollParent = parentElement;targetElement.fastClickScrollParent = parentElement;break;}parentElement = parentElement.parentElement;} while (parentElement);}// 給滾動(dòng)容器加個(gè)標(biāo)志fastClickLastScrollTop,值為其當(dāng)前垂直滾動(dòng)偏移if (scrollParent) {scrollParent.fastClickLastScrollTop = scrollParent.scrollTop;}};另外要注意的是,在 onTouchStart 里被標(biāo)記為 true 的?this.trackingClick 屬性,都會(huì)在其它事件回調(diào)(比如 ontouchmove )的開頭做檢測(cè),如果沒被賦值過,則直接忽略:
if (!this.trackingClick) {return true;}當(dāng)然在 ontouchend 事件里會(huì)把它重置為 false。
2. this.onTouchMove
這段代碼量好少:
FastClick.prototype.onTouchMove = function(event) {//不是需要被追蹤click的事件則忽略if (!this.trackingClick) {return true;}// 如果target突然改變了,或者用戶其實(shí)是在移動(dòng)手勢(shì)而非想要click// 則應(yīng)該清掉this.trackingClick和this.targetElement,告訴后面的事件你們也不用處理了if (this.targetElement !== this.getTargetElementFromEventTarget(event.target) || this.touchHasMoved(event)) {this.trackingClick = false;this.targetElement = null;}return true;};看下這里用到的?this.touchHasMoved 原型方法:
//判斷是否移動(dòng)了//this.touchBoundary是常量,值為10//如果touch已經(jīng)移動(dòng)了10個(gè)偏移量單位,則應(yīng)當(dāng)作為移動(dòng)事件處理而非click事件FastClick.prototype.touchHasMoved = function(event) {var touch = event.changedTouches[0], boundary = this.touchBoundary;if (Math.abs(touch.pageX - this.touchStartX) > boundary || Math.abs(touch.pageY - this.touchStartY) > boundary) {return true;}return false;};3. onTouchEnd
FastClick.prototype.onTouchEnd = function(event) {var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement;if (!this.trackingClick) {return true;}// 避免 phantom 的雙擊(200ms內(nèi)快速點(diǎn)了兩次)觸發(fā) click// 我們?cè)?ontouchstart 里已經(jīng)做過一次判斷了(僅僅禁用默認(rèn)事件),這里再做一次判斷if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {this.cancelNextClick = true; //該屬性會(huì)在 onMouse 事件中被判斷,為true則徹底禁用事件和冒泡return true;}//this.tapTimeout是常量,值為700//識(shí)別是否為長(zhǎng)按事件,如果是(大于700ms)則忽略if ((event.timeStamp - this.trackingClickStart) > this.tapTimeout) {return true;}// 得重置為false,避免input事件被意外取消// 例子見 https://github.com/ftlabs/fastclick/issues/156this.cancelNextClick = false;this.lastClickTime = event.timeStamp; //標(biāo)記touchend時(shí)間,方便下一次的touchstart做雙擊校驗(yàn) trackingClickStart = this.trackingClickStart;//重置 this.trackingClick 和 this.trackingClickStartthis.trackingClick = false;this.trackingClickStart = 0;// iOS 6.0-7.*版本下有個(gè)問題 —— 如果layer處于transition或scroll過程,event所提供的target是不正確的// 所以咱們得重找 targetElement(這里通過 document.elementFromPoint 接口來尋找)if (deviceIsIOSWithBadTarget) { //iOS 6.0-7.*版本touch = event.changedTouches[0]; //手指離開前的觸點(diǎn)// 有些情況下 elementFromPoint 里的參數(shù)是預(yù)期外/不可用的, 所以還得避免 targetElement 為 nulltargetElement = document.elementFromPoint(touch.pageX - window.pageXOffset, touch.pageY - window.pageYOffset) || targetElement;// target可能不正確需要重找,但fastClickScrollParent是不會(huì)變的targetElement.fastClickScrollParent = this.targetElement.fastClickScrollParent;}targetTagName = targetElement.tagName.toLowerCase();if (targetTagName === 'label') { //是label則激活其指向的組件forElement = this.findControl(targetElement);if (forElement) {this.focus(targetElement);//安卓直接返回(無(wú)需合成click事件觸發(fā),因?yàn)辄c(diǎn)擊和激活元素不同,不存在點(diǎn)透)if (deviceIsAndroid) {return false;}targetElement = forElement;}} else if (this.needsFocus(targetElement)) { //非label則識(shí)別是否需要focus的元素//手勢(shì)停留在組件元素時(shí)長(zhǎng)超過100ms,則置空this.targetElement并返回//(而不是通過調(diào)用this.focus來觸發(fā)其聚焦事件,走的原生的click/focus事件觸發(fā)流程)//這也是為何文章開頭提到的問題中,稍微久按一點(diǎn)(超過100ms)textarea是可以把光標(biāo)定位在正確的地方的原因//另外iOS下有個(gè)意料之外的bug——如果被點(diǎn)擊的元素所在文檔是在iframe中的,手動(dòng)調(diào)用其focus的話,//會(huì)發(fā)現(xiàn)你往其中輸入的text是看不到的(即使value做了更新),so這里也直接返回if ((event.timeStamp - trackingClickStart) > 100 || (deviceIsIOS && window.top !== window && targetTagName === 'input')) {this.targetElement = null;return false;}this.focus(targetElement);this.sendClick(targetElement, event); //立即觸發(fā)其click事件,而無(wú)須等待300ms//iOS4下的 select 元素不能禁用默認(rèn)事件(要確保它能被穿透),否則不會(huì)打開select目錄//有時(shí)候 iOS6/7 下(VoiceOver開啟的情況下)也會(huì)如此if (!deviceIsIOS || targetTagName !== 'select') {this.targetElement = null;event.preventDefault();}return false;}if (deviceIsIOS && !deviceIsIOS4) {// 滾動(dòng)容器的垂直滾動(dòng)偏移改變了,說明是容器在做滾動(dòng)而非點(diǎn)擊,則忽略scrollParent = targetElement.fastClickScrollParent;if (scrollParent && scrollParent.fastClickLastScrollTop !== scrollParent.scrollTop) {return true;}}// 查看元素是否無(wú)需處理的白名單內(nèi)(比如加了名為“needsclick”的class)// 不是白名單的則照舊預(yù)防穿透處理,立即觸發(fā)合成的click事件if (!this.needsClick(targetElement)) {event.preventDefault();this.sendClick(targetElement, event);}return false;};這段比較長(zhǎng),我們主要看這段:
} else if (this.needsFocus(targetElement)) { //非label則識(shí)別是否需要focus的元素//手勢(shì)停留在組件元素時(shí)長(zhǎng)超過100ms,則置空this.targetElement并返回//(而不是通過調(diào)用this.focus來觸發(fā)其聚焦事件,走的原生的click/focus事件觸發(fā)流程)//這也是為何文章開頭提到的問題中,稍微久按一點(diǎn)(超過100ms)textarea是可以把光標(biāo)定位在正確的地方的原因//另外iOS下有個(gè)意料之外的bug——如果被點(diǎn)擊的元素所在文檔是在iframe中的,手動(dòng)調(diào)用其focus的話,//會(huì)發(fā)現(xiàn)你往其中輸入的text是看不到的(即使value做了更新),so這里也直接返回if ((event.timeStamp - trackingClickStart) > 100 || (deviceIsIOS && window.top !== window && targetTagName === 'input')) {this.targetElement = null;return false;}this.focus(targetElement);this.sendClick(targetElement, event); //立即觸發(fā)其click事件,而無(wú)須等待300ms//iOS4下的 select 元素不能禁用默認(rèn)事件(要確保它能被穿透),否則不會(huì)打開select目錄//有時(shí)候 iOS6/7 下(VoiceOver開啟的情況下)也會(huì)如此if (!deviceIsIOS || targetTagName !== 'select') {this.targetElement = null;event.preventDefault();}return false;}其中 this.needsFocus 用于判斷給定元素是否需要通過合成click事件來模擬聚焦:
//判斷給定元素是否需要通過合成click事件來模擬聚焦FastClick.prototype.needsFocus = function(target) {switch (target.nodeName.toLowerCase()) {case 'textarea':return true;case 'select':return !deviceIsAndroid; //iOS下的select得走穿透點(diǎn)擊才行case 'input':switch (target.type) {case 'button':case 'checkbox':case 'file':case 'image':case 'radio':case 'submit':return false;}return !target.disabled && !target.readOnly;default://帶有名為“bneedsfocus”的class則返回truereturn (/\bneedsfocus\b/).test(target.className);}};另外這段說明了為何稍微久按一點(diǎn)(超過100ms)textarea ,我們是可以把光標(biāo)定位在正確的地方(會(huì)繞過后面調(diào)用 this.focus 的方法):
//手勢(shì)停留在組件元素時(shí)長(zhǎng)超過100ms,則置空this.targetElement并返回//(而不是通過調(diào)用this.focus來觸發(fā)其聚焦事件,走的原生的click/focus事件觸發(fā)流程)//這也是為何文章開頭提到的問題中,稍微久按一點(diǎn)(超過100ms)textarea是可以把光標(biāo)定位在正確的地方的原因//另外iOS下有個(gè)意料之外的bug——如果被點(diǎn)擊的元素所在文檔是在iframe中的,手動(dòng)調(diào)用其focus的話,//會(huì)發(fā)現(xiàn)你往其中輸入的text是看不到的(即使value做了更新),so這里也直接返回if ((event.timeStamp - trackingClickStart) > 100 || (deviceIsIOS && window.top !== window && targetTagName === 'input')) {this.targetElement = null;return false;}接著咱們看看這兩行很重要的代碼:
this.focus(targetElement);this.sendClick(targetElement, event); //立即觸發(fā)其click事件,而無(wú)須等待300ms所涉及的兩個(gè)原型方法分別為:
⑴ this.focus
FastClick.prototype.focus = function(targetElement) {var length;// 組件建議通過setSelectionRange(selectionStart, selectionEnd)來設(shè)定光標(biāo)范圍(注意這樣還沒有聚焦// 要等到后面觸發(fā) sendClick 事件才會(huì)聚焦)// 另外 iOS7 下有些input元素(比如 date datetime month) 的 selectionStart 和 selectionEnd 特性是沒有整型值的,// 導(dǎo)致會(huì)拋出一個(gè)關(guān)于 setSelectionRange 的模糊錯(cuò)誤,它們需要改用 focus 事件觸發(fā)if (deviceIsIOS && targetElement.setSelectionRange && targetElement.type.indexOf('date') !== 0 && targetElement.type !== 'time' && targetElement.type !== 'month') {length = targetElement.value.length;targetElement.setSelectionRange(length, length);} else {//直接觸發(fā)其focus事件 targetElement.focus();}};注意,我們點(diǎn)擊?textarea 時(shí)調(diào)用了該方法,它通過?targetElement.setSelectionRange(length, length)?決定了光標(biāo)的位置在內(nèi)容的尾部(但注意,這時(shí)候還沒聚焦!!!)。
⑵ this.sendClick
真正讓 textarea 聚焦的是這個(gè)方法,它合成了一個(gè) click 方法立刻在textarea元素上觸發(fā)導(dǎo)致聚焦:
//合成一個(gè)click事件并在指定元素上觸發(fā)FastClick.prototype.sendClick = function(targetElement, event) {var clickEvent, touch;// 在一些安卓機(jī)器中,得讓頁(yè)面所存在的 activeElement(聚焦的元素,比如input)失焦,否則合成的click事件將無(wú)效if (document.activeElement && document.activeElement !== targetElement) {document.activeElement.blur();}touch = event.changedTouches[0];// 合成(Synthesise) 一個(gè) click 事件// 通過一個(gè)額外屬性確保它能被追蹤(tracked)clickEvent = 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; // fastclick的內(nèi)部變量,用來識(shí)別click事件是原生還是合成的targetElement.dispatchEvent(clickEvent); //立即觸發(fā)其click事件 };FastClick.prototype.determineEventType = function(targetElement) {//安卓設(shè)備下 Select 無(wú)法通過合成的 click 事件被展開,得改為 mousedownif (deviceIsAndroid && targetElement.tagName.toLowerCase() === 'select') {return 'mousedown';}return 'click';};經(jīng)過這么一折騰,咱們輕點(diǎn) textarea 后,光標(biāo)就自然定位到其內(nèi)容尾部去了。但是這里有個(gè)問題——排在 touchend 后的 focus 事件為啥沒被觸發(fā)呢?
如果 focus 事件能被觸發(fā)的話,那肯定能重新定位光標(biāo)到正確的位置呀。
咱們看下面這段:
//iOS4下的 select 元素不能禁用默認(rèn)事件(要確保它能被穿透),否則不會(huì)打開select目錄//有時(shí)候 iOS6/7 下(VoiceOver開啟的情況下)也會(huì)如此if (!deviceIsIOS || targetTagName !== 'select' ) {this.targetElement = null;event.preventDefault();}通過 preventDefault 的阻擋,textarea 自然再也無(wú)法擁抱其 focus 寶寶了~
于是乎,我們?cè)谶@里做個(gè)改動(dòng)就能修復(fù)這個(gè)問題:
var _isTextInput = function(){return targetTagName === 'textarea' || (targetTagName === 'input' && targetElement.type === 'text');};if ((!deviceIsIOS || targetTagName !== 'select') && !_isTextInput()) {this.targetElement = null;event.preventDefault();}或者:
if (!deviceIsIOS4 || targetTagName !== 'select') {this.targetElement = null;//給textarea加上“needsclick”的classif((!/\bneedsclick\b/).test(targetElement.className)){event.preventDefault(); } }這里要吐槽下的是,Fastclick 把?this.needsClick 放到了 ontouchEnd 末尾去執(zhí)行,才導(dǎo)致前面說的加上了“needsclick”類名也無(wú)效的問題。
雖然問題原因找到也解決了,但咱們還是繼續(xù)看剩下的部分吧。
4. onMouse 和 onClick
//用于決定是否允許穿透事件(觸發(fā)layer的click默認(rèn)事件)FastClick.prototype.onMouse = function(event) {// touch事件一直沒觸發(fā)if (!this.targetElement) {return true;}if (event.forwardedTouchEvent) { //觸發(fā)的click事件是合成的return true;}// 編程派生的事件所對(duì)應(yīng)元素事件可以被允許// 確保其沒執(zhí)行過 preventDefault 方法(event.cancelable 不為 true)即可if (!event.cancelable) {return true;}// 需要做預(yù)防穿透處理的元素,或者做了快速(200ms)雙擊的情況if (!this.needsClick(this.targetElement) || this.cancelNextClick) {//停止當(dāng)前默認(rèn)事件和冒泡if (event.stopImmediatePropagation) {event.stopImmediatePropagation();} else {// 不支持 stopImmediatePropagation 的設(shè)備(比如Android 2)做標(biāo)記,// 確保該事件回調(diào)不會(huì)執(zhí)行(見126行)event.propagationStopped = true;}// 取消事件和冒泡 event.stopPropagation();event.preventDefault();return false;}//允許穿透return true;};//click事件常規(guī)都是touch事件衍生來的,也排在touch后面觸發(fā)。//對(duì)于那些我們?cè)趖ouch事件過程沒有禁用掉默認(rèn)事件的event來說,我們還需要在click的捕獲階段進(jìn)一步//做判斷決定是否要禁掉點(diǎn)擊事件(防穿透)FastClick.prototype.onClick = function(event) {var permitted;// 如果還有 trackingClick 存在,可能是某些UI事件阻塞了touchEnd 的執(zhí)行if (this.trackingClick) {this.targetElement = null;this.trackingClick = false;return true;}// 依舊是對(duì) iOS 怪異行為的處理 —— 如果用戶點(diǎn)擊了iOS模擬器里某個(gè)表單中的一個(gè)submit元素// 或者點(diǎn)擊了彈出來的鍵盤里的“Go”按鈕,會(huì)觸發(fā)一個(gè)“偽”click事件(target是一個(gè)submit-type的input元素)if (event.target.type === 'submit' && event.detail === 0) {return true;}permitted = this.onMouse(event);if (!permitted) { //如果點(diǎn)擊是被允許的,將this.targetElement置空可以確保onMouse事件里不會(huì)阻止默認(rèn)事件this.targetElement = null;}//沒有多大意義return permitted;};//銷毀Fastclick所注冊(cè)的監(jiān)聽事件。是給外部實(shí)例去調(diào)用的FastClick.prototype.destroy = function() {var layer = this.layer;if (deviceIsAndroid) {layer.removeEventListener('mouseover', this.onMouse, true);layer.removeEventListener('mousedown', this.onMouse, true);layer.removeEventListener('mouseup', this.onMouse, true);}layer.removeEventListener('click', this.onClick, true);layer.removeEventListener('touchstart', this.onTouchStart, false);layer.removeEventListener('touchmove', this.onTouchMove, false);layer.removeEventListener('touchend', this.onTouchEnd, false);layer.removeEventListener('touchcancel', this.onTouchCancel, false);};常規(guī)需要阻斷點(diǎn)擊事件的操作,我們?cè)?touch 監(jiān)聽事件回調(diào)中已經(jīng)做了處理,這里主要是針對(duì)那些 touch 過程(有些設(shè)備甚至可能并沒有touch事件觸發(fā))沒有禁用默認(rèn)事件的 event 做進(jìn)一步處理,從而決定是否觸發(fā)原生的 click 事件(如果禁止是在 onMouse 方法里做的處理)。
小結(jié)
1. 在 fastclick 源碼的 addEventListener?回調(diào)事件中有很多的 return false/true。它們其實(shí)主要用于繞過后面的腳本邏輯,并沒有其它意義(它是不會(huì)阻止默認(rèn)事件的)。
所以千萬(wàn)別把 jQuery 事件、或者 DOM0 級(jí)事件回調(diào)中的 return false 概念,跟 addEventListener 的混在一起了。
2. fastclick 的源碼其實(shí)很簡(jiǎn)單,有很大部分不外乎對(duì)一些怪異行為做 hack,其核心理念不外乎是——捕獲 target 事件,判斷 target 是要解決點(diǎn)透問題的元素,就合成一個(gè) click 事件在 target 上觸發(fā),同時(shí)通過 preventDefault 禁用默認(rèn)事件。
3. fastclick 雖好,但也有一些坑,還是得按需求對(duì)其修改,那么了解其源碼還是很有必要的。
轉(zhuǎn)載于:https://www.cnblogs.com/vajoy/p/5522114.html
總結(jié)
以上是生活随笔為你收集整理的FastClick 填坑及源码解析的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 图数据库――大数据时代的高铁
- 下一篇: 大数据Map Reduce 和 MPP数