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