在分析fastclcik源碼以前須要先搞清楚爲何非得用click代替touchstart,移動端直接使用touchstart不就好了嗎。我認爲主要有如下兩大理由:javascript
一、部分網站PC端、移動端共用一套代碼,都綁定了touchstart,PC端還怎麼玩css
二、兩者觸發條件不一樣:a)touchstart 手指觸摸到顯示屏即觸發事件 b)click 手指觸摸到顯示屏,不曾在屏幕上移動(或移動一個很是小的位移值),而後手指離開屏幕,從觸摸到離開時間間隔較短,此時纔會觸發click事件。html
click體驗要明顯好於touchstart,故咱們要爲click填坑。html5
通過一段時間修改測試寫下以下代碼,運行效果還行:java
1 <!doctype html> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimun-scale=1.0"> 6 <title>手機端點擊</title> 7 </head> 8 <body> 9 <div id="demo1" style="width:100px;height:100px;background:red;"></div> 10 <div id="demo2" style="width:100px;height:100px;background:blue;"></div> 11 <script> 12 var demo1 = document.querySelector('#demo1'), demo2 = document.querySelector("#demo2") 13 demo1.addEventListener('touchstart', function(){ 14 demo1.innerHTML=demo1.innerHTML + "<br>touchstart" 15 }) 16 demo2.addEventListener('click', function (e) { 17 if(!e.ming){ 18 e.preventDefault(); 19 return 20 } 21 demo2.innerHTML=demo2.innerHTML + "<br>click" 22 }) 23 24 var el 25 document.addEventListener("touchstart", function(e){ 26 el = e.target 27 }) 28 document.addEventListener("touchend", function(e){ 29 var event = document.createEvent("MouseEvents") 30 event.initEvent("click", true, true) 31 event.ming = true 32 el && el.dispatchEvent(event) 33 }) 34 </script> 35 </body> 36 </html>
在用個人IOS9瀏覽器測試時,效果還行。就是點擊demo2時會有閃爍發生,並有一個黑框。通過測試,黑框是outline,閃爍是click觸發了瀏覽器的重繪。outline設爲none便可,閃爍暫時沒找到方法避免,這不是本文重點,之後再研究。node
你們喜歡用fastclick還有個緣由是它能夠避免點透,如今先看看咱們的代碼能不能避免點透,先搞個例子:android
<!doctype html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimun-scale=1.0"> <title>手機端點擊</title> <style> body{margin:0;} input{width:100%;height:20px;} #demo1{padding-top:20px;} #demo2{width:100%;background: #ebebe7;height:200px;position:absolute;top:0;text-align:center;padding-top:20px;} #btn{background: #fff;height:25px;display:inline-block;color:red;opacity:1;} .hide{display:none;} </style> </head> <body> <div id="demo1"> <input id="text"> </div> <div id="demo2"> <button id="btn">點擊我</button> </div> <script> var demo1 = document.querySelector('#demo1'), demo2 = document.querySelector("#demo2"), btn = document.querySelector("#btn") btn.addEventListener('click', function(e){ if(!e.ming){ e.preventDefault(); e.stopPropagation(); return } demo2.className = "hide"; }) var el document.addEventListener("touchstart", function(e){ el = e.target }) document.addEventListener("touchend", function(e){ var event = document.createEvent("MouseEvents") event.initEvent("click", true, true) event.ming = true el && el.dispatchEvent(event) }) </script> </body> </html>
執行代碼,順利點透git
將touchend默認事件阻止便可,這時就不會再出現點透問題,但悲劇的是input永遠也獲取不到焦點了github
<!doctype html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <title>手機端點擊</title> <style> body{margin:0;} input{width:90%;height:20px;} #demo1{padding-top:20px;} #demo2{width:100%;background: #ebebe7;height:200px;position:absolute;top:0;text-align:center;padding-top:20px;} #btn{background: #fff;height:25px;display:inline-block;color:red;opacity:1;} .hide{display:none;} </style> </head> <body> <div id="demo1"> <input id="text"> </div> <div id="demo2"> <button id="btn">點擊我</button> </div> <script> var demo1 = document.querySelector('#demo1'), demo2 = document.querySelector("#demo2"), btn = document.querySelector("#btn") document.addEventListener('click', function(e){ if(e.ming){ return true; } if (e.stopImmediatePropagation) { e.stopImmediatePropagation(); } else { e.propagationStopped = true; } e.stopPropagation(); e.preventDefault(); return true; }, true) btn.addEventListener('click', function(e){ demo2.className = "hide"; }) var el document.addEventListener("touchstart", function(e){ el = e.target }) document.addEventListener("touchend", function(e){ e.preventDefault(); var event = document.createEvent("MouseEvents") event.initEvent("click", true, true) event.ming = true el && el.dispatchEvent(event) }) </script> </body> </html>
此處分析:咱們知道點擊後事件順序以下:touchstart、touchend、click,touchend觸發後懸浮框demo2隱藏,瀏覽器自帶的click被阻止了默認操做,怎麼還會點透呢。測試以下:web
<!doctype html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <title>手機端點擊</title> <style> body{margin:0;} input{width:90%;height:20px;} #demo1{padding-top:20px;} #demo2{width:100%;background: #ebebe7;height:200px;position:absolute;top:0;text-align:center;padding-top:20px;} #btn{background: #fff;height:25px;display:inline-block;color:red;opacity:1;} .hide{display:none;} </style> </head> <body> <div id="demo1"> <input id="text"> </div> <div id="demo2"> <button id="btn">點擊我</button> </div> <script> var demo1 = document.querySelector('#demo1'), demo2 = document.querySelector("#demo2"), btn = document.querySelector("#btn"), text = document.querySelector("#text") document.addEventListener('click', function(e){ if(e.ming){ return true; } if (e.stopImmediatePropagation) { e.stopImmediatePropagation(); } else { e.propagationStopped = true; } e.stopPropagation(); e.preventDefault(); return true; }, true) /*document.addEventListener('mousedown', function(e){ if(e.ming){ return true; } if (e.stopImmediatePropagation) { e.stopImmediatePropagation(); } else { e.propagationStopped = true; } e.stopPropagation(); e.preventDefault(); return true; }, true)*/ text.addEventListener("click", function(){ console.log("text click") }) text.addEventListener("touchend", function(){ console.log("text touchend") }) text.addEventListener("touchstart", function(){ console.log("text touchstart") }) text.addEventListener("mousedown", function(){ console.log("text mousedown") }) btn.addEventListener('click', function(e){ console.log(e.ming); demo2.className = "hide"; }) var el document.addEventListener("touchstart", function(e){ el = e.target }) document.addEventListener("touchend", function(e){ console.log('touchend') var event = document.createEvent("MouseEvents") event.initEvent("click", true, true) event.ming = true el && el.dispatchEvent(event) }) </script> </body> </html>
結果發現input上只有mousedown被觸發了,原來是mousedown搞的事,手機點擊後觸發事件正確順序是:touchstart、touchend、click、mousedown。該怎麼阻止mosedown呢。
這裏研究一下阻止哪些事件後input沒法獲取焦點
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title></title> <meta name="viewport" content="width=device-width,initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"> <style type="text/css"> .bt { position: absolute; top: 50px; display: block; height: 50px; } </style> </head> <body> <input id="input1"> <input id="input2"> <input id="input3"> <input id="input4"> <input id="input5"> <input id="input6"> </body> <script type="text/javascript"> var input1 = document.querySelector('#input1'), input2 = document.querySelector('#input2'), input3 = document.querySelector('#input3') ,input4 = document.querySelector('#input4'), input5 = document.querySelector('#input5'),input6 = document.querySelector('#input6') input1.addEventListener('touchstart', function(e){ e.preventDefault(); }) input2.addEventListener('touchend', function(e){ e.preventDefault(); }) input3.addEventListener('click', function(e){ e.preventDefault(); }) input4.addEventListener('mousedown', function(e){ e.preventDefault(); }) input5.addEventListener('mouseout', function(e){ e.preventDefault(); }) input6.addEventListener('mouseenter', function(e){ e.preventDefault(); }) </script> </html>
手機測試發現,touchstart、touchend、mousedown事件被阻止後,input就沒法再獲取焦點。而阻止click事件並不阻止獲取焦點。ok,來看看fastclick的解決方案。
1 ;(function () { 2 'use strict'; 3 4 /** 5 * @preserve FastClick: polyfill to remove click delays on browsers with touch UIs. 6 * 7 * @codingstandard ftlabs-jsv2 8 * @copyright The Financial Times Limited [All Rights Reserved] 9 * @license MIT License (see LICENSE.txt) 10 */ 11 12 /*jslint browser:true, node:true*/ 13 /*global define, Event, Node*/ 14 15 16 /** 17 * Instantiate fast-clicking listeners on the specified layer. 18 * 19 * @constructor 20 * @param {Element} layer The layer to listen on 21 * @param {Object} [options={}] The options to override the defaults 22 */ 23 function FastClick(layer, options) { 24 var oldOnClick; 25 26 options = options || {}; 27 28 /** 29 * Whether a click is currently being tracked. 30 * 31 * @type boolean 32 */ 33 this.trackingClick = false; 34 35 36 /** 37 * Timestamp for when click tracking started. 38 * 39 * @type number 40 */ 41 this.trackingClickStart = 0; 42 43 44 /** 45 * The element being tracked for a click. 46 * 47 * @type EventTarget 48 */ 49 this.targetElement = null; 50 51 52 /** 53 * X-coordinate of touch start event. 54 * 55 * @type number 56 */ 57 this.touchStartX = 0; 58 59 60 /** 61 * Y-coordinate of touch start event. 62 * 63 * @type number 64 */ 65 this.touchStartY = 0; 66 67 68 /** 69 * ID of the last touch, retrieved from Touch.identifier. 70 * 71 * @type number 72 */ 73 this.lastTouchIdentifier = 0; 74 75 76 /** 77 * Touchmove boundary, beyond which a click will be cancelled. 78 * 79 * @type number 80 */ 81 this.touchBoundary = options.touchBoundary || 10; 82 83 84 /** 85 * The FastClick layer. 86 * 87 * @type Element 88 */ 89 this.layer = layer; 90 91 /** 92 * The minimum time between tap(touchstart and touchend) events 93 * 94 * @type number 95 */ 96 this.tapDelay = options.tapDelay || 200; 97 98 /** 99 * The maximum time for a tap 100 * 101 * @type number 102 */ 103 this.tapTimeout = options.tapTimeout || 700; 104 105 //部分瀏覽器click已經不會延遲300ms 不須要使用fastclick了 106 if (FastClick.notNeeded(layer)) { 107 return; 108 } 109 110 // Some old versions of Android don't have Function.prototype.bind 111 function bind(method, context) { 112 return function() { return method.apply(context, arguments); }; 113 } 114 115 //須要監聽的事件 116 var methods = ['onMouse', 'onClick', 'onTouchStart', 'onTouchMove', 'onTouchEnd', 'onTouchCancel']; 117 var context = this; 118 for (var i = 0, l = methods.length; i < l; i++) { 119 context[methods[i]] = bind(context[methods[i]], context); 120 } 121 122 //android除了touch事件,還須要監視mouse事件 123 // Set up event handlers as required 124 if (deviceIsAndroid) { 125 layer.addEventListener('mouseover', this.onMouse, true); 126 layer.addEventListener('mousedown', this.onMouse, true); 127 layer.addEventListener('mouseup', this.onMouse, true); 128 } 129 130 layer.addEventListener('click', this.onClick, true); 131 layer.addEventListener('touchstart', this.onTouchStart, false); 132 layer.addEventListener('touchmove', this.onTouchMove, false); 133 layer.addEventListener('touchend', this.onTouchEnd, false); 134 layer.addEventListener('touchcancel', this.onTouchCancel, false); 135 136 // Hack is required for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2) 137 // which is how FastClick normally stops click events bubbling to callbacks registered on the FastClick 138 // layer when they are cancelled. 139 if (!Event.prototype.stopImmediatePropagation) { 140 layer.removeEventListener = function(type, callback, capture) { 141 var rmv = Node.prototype.removeEventListener; 142 if (type === 'click') { 143 rmv.call(layer, type, callback.hijacked || callback, capture); 144 } else { 145 rmv.call(layer, type, callback, capture); 146 } 147 }; 148 149 layer.addEventListener = function(type, callback, capture) { 150 var adv = Node.prototype.addEventListener; 151 if (type === 'click') { 152 adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) { 153 if (!event.propagationStopped) { 154 callback(event); 155 } 156 }), capture); 157 } else { 158 adv.call(layer, type, callback, capture); 159 } 160 }; 161 } 162 163 // If a handler is already declared in the element's onclick attribute, it will be fired before 164 // FastClick's onClick handler. Fix this by pulling out the user-defined handler function and 165 // adding it as listener. 166 if (typeof layer.onclick === 'function') { 167 168 // Android browser on at least 3.2 requires a new reference to the function in layer.onclick 169 // - the old one won't work if passed to addEventListener directly. 170 oldOnClick = layer.onclick; 171 layer.addEventListener('click', function(event) { 172 oldOnClick(event); 173 }, false); 174 layer.onclick = null; 175 } 176 } 177 178 /** 179 * Windows Phone 8.1 fakes user agent string to look like Android and iPhone. 180 * 181 * @type boolean 182 */ 183 var deviceIsWindowsPhone = navigator.userAgent.indexOf("Windows Phone") >= 0; 184 185 /** 186 * Android requires exceptions. 187 * 188 * @type boolean 189 */ 190 var deviceIsAndroid = navigator.userAgent.indexOf('Android') > 0 && !deviceIsWindowsPhone; 191 192 193 /** 194 * iOS requires exceptions. 195 * 196 * @type boolean 197 */ 198 var deviceIsIOS = /iP(ad|hone|od)/.test(navigator.userAgent) && !deviceIsWindowsPhone; 199 200 201 /** 202 * iOS 4 requires an exception for select elements. 203 * 204 * @type boolean 205 */ 206 var deviceIsIOS4 = deviceIsIOS && (/OS 4_\d(_\d)?/).test(navigator.userAgent); 207 208 209 /** 210 * iOS 6.0-7.* requires the target element to be manually derived 211 * 212 * @type boolean 213 */ 214 var deviceIsIOSWithBadTarget = deviceIsIOS && (/OS [6-7]_\d/).test(navigator.userAgent); 215 216 /** 217 * BlackBerry requires exceptions. 218 * 219 * @type boolean 220 */ 221 var deviceIsBlackBerry10 = navigator.userAgent.indexOf('BB10') > 0; 222 223 /** 224 * Determine whether a given element requires a native click. 225 * 226 * @param {EventTarget|Element} target Target DOM element 227 * @returns {boolean} Returns true if the element needs a native click 228 */ 229 FastClick.prototype.needsClick = function(target) { 230 switch (target.nodeName.toLowerCase()) { 231 232 // Don't send a synthetic click to disabled inputs (issue #62) 233 case 'button': 234 case 'select': 235 case 'textarea': 236 if (target.disabled) { 237 return true; 238 } 239 240 break; 241 case 'input': 242 243 // File inputs need real clicks on iOS 6 due to a browser bug (issue #68) 244 if ((deviceIsIOS && target.type === 'file') || target.disabled) { 245 return true; 246 } 247 248 break; 249 case 'label': 250 case 'iframe': // iOS8 homescreen apps can prevent events bubbling into frames 251 case 'video': 252 return true; 253 } 254 255 return (/\bneedsclick\b/).test(target.className); 256 }; 257 258 259 /** 260 * Determine whether a given element requires a call to focus to simulate click into element. 261 * 262 * @param {EventTarget|Element} target Target DOM element 263 * @returns {boolean} Returns true if the element requires a call to focus to simulate native click. 264 */ 265 FastClick.prototype.needsFocus = function(target) { 266 switch (target.nodeName.toLowerCase()) { 267 case 'textarea': 268 return true; 269 case 'select': 270 return !deviceIsAndroid; 271 case 'input': 272 switch (target.type) { 273 case 'button': 274 case 'checkbox': 275 case 'file': 276 case 'image': 277 case 'radio': 278 case 'submit': 279 return false; 280 } 281 282 // No point in attempting to focus disabled inputs 283 return !target.disabled && !target.readOnly; 284 default: 285 return (/\bneedsfocus\b/).test(target.className); 286 } 287 }; 288 289 290 /** 291 * Send a click event to the specified element. 292 * 293 * @param {EventTarget|Element} targetElement 294 * @param {Event} event 295 */ 296 FastClick.prototype.sendClick = function(targetElement, event) { 297 var clickEvent, touch; 298 299 // On some Android devices activeElement needs to be blurred otherwise the synthetic click will have no effect (#24) 300 if (document.activeElement && document.activeElement !== targetElement) { 301 document.activeElement.blur(); 302 } 303 304 touch = event.changedTouches[0]; 305 306 // Synthesise a click event, with an extra attribute so it can be tracked 307 clickEvent = document.createEvent('MouseEvents'); 308 clickEvent.initMouseEvent(this.determineEventType(targetElement), true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null); 309 clickEvent.forwardedTouchEvent = true; 310 targetElement.dispatchEvent(clickEvent); 311 }; 312 313 FastClick.prototype.determineEventType = function(targetElement) { 314 315 //Issue #159: Android Chrome Select Box does not open with a synthetic click event 316 if (deviceIsAndroid && targetElement.tagName.toLowerCase() === 'select') { 317 return 'mousedown'; 318 } 319 320 return 'click'; 321 }; 322 323 324 /** 325 * @param {EventTarget|Element} targetElement 326 */ 327 FastClick.prototype.focus = function(targetElement) { 328 var length; 329 330 // Issue #160: on iOS 7, some input elements (e.g. date datetime month) throw a vague TypeError on setSelectionRange. These elements don't have an integer value for the selectionStart and selectionEnd properties, but unfortunately that can't be used for detection because accessing the properties also throws a TypeError. Just check the type instead. Filed as Apple bug #15122724. 331 if (deviceIsIOS && targetElement.setSelectionRange && targetElement.type.indexOf('date') !== 0 && targetElement.type !== 'time' && targetElement.type !== 'month') { 332 length = targetElement.value.length; 333 targetElement.setSelectionRange(length, length); 334 } else { 335 targetElement.focus(); 336 } 337 }; 338 339 340 /** 341 * Check whether the given target element is a child of a scrollable layer and if so, set a flag on it. 342 * 343 * @param {EventTarget|Element} targetElement 344 */ 345 FastClick.prototype.updateScrollParent = function(targetElement) { 346 var scrollParent, parentElement; 347 348 scrollParent = targetElement.fastClickScrollParent; 349 350 // Attempt to discover whether the target element is contained within a scrollable layer. Re-check if the 351 // target element was moved to another parent. 352 if (!scrollParent || !scrollParent.contains(targetElement)) { 353 parentElement = targetElement; 354 do { 355 if (parentElement.scrollHeight > parentElement.offsetHeight) { 356 scrollParent = parentElement; 357 targetElement.fastClickScrollParent = parentElement; 358 break; 359 } 360 361 parentElement = parentElement.parentElement; 362 } while (parentElement); 363 } 364 365 // Always update the scroll top tracker if possible. 366 if (scrollParent) { 367 scrollParent.fastClickLastScrollTop = scrollParent.scrollTop; 368 } 369 }; 370 371 372 /** 373 * @param {EventTarget} targetElement 374 * @returns {Element|EventTarget} 375 */ 376 FastClick.prototype.getTargetElementFromEventTarget = function(eventTarget) { 377 378 // On some older browsers (notably Safari on iOS 4.1 - see issue #56) the event target may be a text node. 379 if (eventTarget.nodeType === Node.TEXT_NODE) { 380 return eventTarget.parentNode; 381 } 382 383 return eventTarget; 384 }; 385 386 387 /** 388 * On touch start, record the position and scroll offset. 389 * 390 * @param {Event} event 391 * @returns {boolean} 392 */ 393 FastClick.prototype.onTouchStart = function(event) { 394 var targetElement, touch, selection; 395 396 // Ignore multiple touches, otherwise pinch-to-zoom is prevented if both fingers are on the FastClick element (issue #111). 397 if (event.targetTouches.length > 1) { 398 return true; 399 } 400 401 targetElement = this.getTargetElementFromEventTarget(event.target); 402 touch = event.targetTouches[0]; 403 404 if (deviceIsIOS) { 405 406 // Only trusted events will deselect text on iOS (issue #49) 407 selection = window.getSelection(); 408 if (selection.rangeCount && !selection.isCollapsed) { 409 return true; 410 } 411 412 if (!deviceIsIOS4) { 413 414 // Weird things happen on iOS when an alert or confirm dialog is opened from a click event callback (issue #23): 415 // when the user next taps anywhere else on the page, new touchstart and touchend events are dispatched 416 // with the same identifier as the touch event that previously triggered the click that triggered the alert. 417 // Sadly, there is an issue on iOS 4 that causes some normal touch events to have the same identifier as an 418 // immediately preceeding touch event (issue #52), so this fix is unavailable on that platform. 419 // Issue 120: touch.identifier is 0 when Chrome dev tools 'Emulate touch events' is set with an iOS device UA string, 420 // which causes all touch events to be ignored. As this block only applies to iOS, and iOS identifiers are always long, 421 // random integers, it's safe to to continue if the identifier is 0 here. 422 if (touch.identifier && touch.identifier === this.lastTouchIdentifier) { 423 event.preventDefault(); 424 return false; 425 } 426 427 this.lastTouchIdentifier = touch.identifier; 428 429 // If the target element is a child of a scrollable layer (using -webkit-overflow-scrolling: touch) and: 430 // 1) the user does a fling scroll on the scrollable layer 431 // 2) the user stops the fling scroll with another tap 432 // then the event.target of the last 'touchend' event will be the element that was under the user's finger 433 // when the fling scroll was started, causing FastClick to send a click event to that layer - unless a check 434 // is made to ensure that a parent layer was not scrolled before sending a synthetic click (issue #42). 435 this.updateScrollParent(targetElement); 436 } 437 } 438 439 this.trackingClick = true; 440 this.trackingClickStart = event.timeStamp; 441 this.targetElement = targetElement; 442 443 this.touchStartX = touch.pageX; 444 this.touchStartY = touch.pageY; 445 446 // Prevent phantom clicks on fast double-tap (issue #36) 447 if ((event.timeStamp - this.lastClickTime) < this.tapDelay) { 448 event.preventDefault(); 449 } 450 451 return true; 452 }; 453 454 455 /** 456 * Based on a touchmove event object, check whether the touch has moved past a boundary since it started. 457 * 458 * @param {Event} event 459 * @returns {boolean} 460 */ 461 FastClick.prototype.touchHasMoved = function(event) { 462 var touch = event.changedTouches[0], boundary = this.touchBoundary; 463 464 if (Math.abs(touch.pageX - this.touchStartX) > boundary || Math.abs(touch.pageY - this.touchStartY) > boundary) { 465 return true; 466 } 467 468 return false; 469 }; 470 471 472 /** 473 * Update the last position. 474 * 475 * @param {Event} event 476 * @returns {boolean} 477 */ 478 FastClick.prototype.onTouchMove = function(event) { 479 if (!this.trackingClick) { 480 return true; 481 } 482 483 // If the touch has moved, cancel the click tracking 484 if (this.targetElement !== this.getTargetElementFromEventTarget(event.target) || this.touchHasMoved(event)) { 485 this.trackingClick = false; 486 this.targetElement = null; 487 } 488 489 return true; 490 }; 491 492 493 /** 494 * Attempt to find the labelled control for the given label element. 495 * 496 * @param {EventTarget|HTMLLabelElement} labelElement 497 * @returns {Element|null} 498 */ 499 FastClick.prototype.findControl = function(labelElement) { 500 501 // Fast path for newer browsers supporting the HTML5 control attribute 502 if (labelElement.control !== undefined) { 503 return labelElement.control; 504 } 505 506 // All browsers under test that support touch events also support the HTML5 htmlFor attribute 507 if (labelElement.htmlFor) { 508 return document.getElementById(labelElement.htmlFor); 509 } 510 511 // If no for attribute exists, attempt to retrieve the first labellable descendant element 512 // the list of which is defined here: http://www.w3.org/TR/html5/forms.html#category-label 513 return labelElement.querySelector('button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea'); 514 }; 515 516 517 /** 518 * On touch end, determine whether to send a click event at once. 519 * 520 * @param {Event} event 521 * @returns {boolean} 522 */ 523 FastClick.prototype.onTouchEnd = function(event) { 524 var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement; 525 526 if (!this.trackingClick) { 527 return true; 528 } 529 530 // Prevent phantom clicks on fast double-tap (issue #36) 531 if ((event.timeStamp - this.lastClickTime) < this.tapDelay) { 532 this.cancelNextClick = true; 533 return true; 534 } 535 536 if ((event.timeStamp - this.trackingClickStart) > this.tapTimeout) { 537 return true; 538 } 539 540 // Reset to prevent wrong click cancel on input (issue #156). 541 this.cancelNextClick = false; 542 543 this.lastClickTime = event.timeStamp; 544 545 trackingClickStart = this.trackingClickStart; 546 this.trackingClick = false; 547 this.trackingClickStart = 0; 548 549 // On some iOS devices, the targetElement supplied with the event is invalid if the layer 550 // is performing a transition or scroll, and has to be re-detected manually. Note that 551 // for this to function correctly, it must be called *after* the event target is checked! 552 // See issue #57; also filed as rdar://13048589 . 553 if (deviceIsIOSWithBadTarget) { 554 touch = event.changedTouches[0]; 555 556 // In certain cases arguments of elementFromPoint can be negative, so prevent setting targetElement to null 557 targetElement = document.elementFromPoint(touch.pageX - window.pageXOffset, touch.pageY - window.pageYOffset) || targetElement; 558 targetElement.fastClickScrollParent = this.targetElement.fastClickScrollParent; 559 } 560 561 targetTagName = targetElement.tagName.toLowerCase(); 562 if (targetTagName === 'label') { 563 forElement = this.findControl(targetElement); 564 if (forElement) { 565 this.focus(targetElement); 566 if (deviceIsAndroid) { 567 return false; 568 } 569 570 targetElement = forElement; 571 } 572 } else if (this.needsFocus(targetElement)) { 573 574 // Case 1: If the touch started a while ago (best guess is 100ms based on tests for issue #36) then focus will be triggered anyway. Return early and unset the target element reference so that the subsequent click will be allowed through. 575 // Case 2: Without this exception for input elements tapped when the document is contained in an iframe, then any inputted text won't be visible even though the value attribute is updated as the user types (issue #37). 576 if ((event.timeStamp - trackingClickStart) > 100 || (deviceIsIOS && window.top !== window && targetTagName === 'input')) { 577 this.targetElement = null; 578 return false; 579 } 580 581 this.focus(targetElement); 582 this.sendClick(targetElement, event); 583 584 // Select elements need the event to go through on iOS 4, otherwise the selector menu won't open. 585 // Also this breaks opening selects when VoiceOver is active on iOS6, iOS7 (and possibly others) 586 if (!deviceIsIOS || targetTagName !== 'select') { 587 this.targetElement = null; 588 event.preventDefault(); 589 } 590 591 return false; 592 } 593 594 if (deviceIsIOS && !deviceIsIOS4) { 595 596 // Don't send a synthetic click event if the target element is contained within a parent layer that was scrolled 597 // and this tap is being used to stop the scrolling (usually initiated by a fling - issue #42). 598 scrollParent = targetElement.fastClickScrollParent; 599 if (scrollParent && scrollParent.fastClickLastScrollTop !== scrollParent.scrollTop) { 600 return true; 601 } 602 } 603 604 // Prevent the actual click from going though - unless the target node is marked as requiring 605 // real clicks or if it is in the whitelist in which case only non-programmatic clicks are permitted. 606 if (!this.needsClick(targetElement)) { 607 event.preventDefault(); 608 this.sendClick(targetElement, event); 609 } 610 611 return false; 612 }; 613 614 615 /** 616 * On touch cancel, stop tracking the click. 617 * 618 * @returns {void} 619 */ 620 FastClick.prototype.onTouchCancel = function() { 621 this.trackingClick = false; 622 this.targetElement = null; 623 }; 624 625 626 /** 627 * Determine mouse events which should be permitted. 628 * 629 * @param {Event} event 630 * @returns {boolean} 631 */ 632 FastClick.prototype.onMouse = function(event) { 633 634 // If a target element was never set (because a touch event was never fired) allow the event 635 if (!this.targetElement) { 636 return true; 637 } 638 639 if (event.forwardedTouchEvent) { 640 return true; 641 } 642 643 // Programmatically generated events targeting a specific element should be permitted 644 if (!event.cancelable) { 645 return true; 646 } 647 648 // Derive and check the target element to see whether the mouse event needs to be permitted; 649 // unless explicitly enabled, prevent non-touch click events from triggering actions, 650 // to prevent ghost/doubleclicks. 651 if (!this.needsClick(this.targetElement) || this.cancelNextClick) { 652 653 // Prevent any user-added listeners declared on FastClick element from being fired. 654 if (event.stopImmediatePropagation) { 655 event.stopImmediatePropagation(); 656 } else { 657 658 // Part of the hack for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2) 659 event.propagationStopped = true; 660 } 661 662 // Cancel the event 663 event.stopPropagation(); 664 event.preventDefault(); 665 666 return false; 667 } 668 669 // If the mouse event is permitted, return true for the action to go through. 670 return true; 671 }; 672 673 674 /** 675 * On actual clicks, determine whether this is a touch-generated click, a click action occurring 676 * naturally after a delay after a touch (which needs to be cancelled to avoid duplication), or 677 * an actual click which should be permitted. 678 * 679 * @param {Event} event 680 * @returns {boolean} 681 */ 682 FastClick.prototype.onClick = function(event) { 683 var permitted; 684 685 // It's possible for another FastClick-like library delivered with third-party code to fire a click event before FastClick does (issue #44). In that case, set the click-tracking flag back to false and return early. This will cause onTouchEnd to return early. 686 if (this.trackingClick) { 687 this.targetElement = null; 688 this.trackingClick = false; 689 return true; 690 } 691 692 // Very odd behaviour on iOS (issue #18): if a submit element is present inside a form and the user hits enter in the iOS simulator or clicks the Go button on the pop-up OS keyboard the a kind of 'fake' click event will be triggered with the submit-type input element as the target. 693 if (event.target.type === 'submit' && event.detail === 0) { 694 return true; 695 } 696 697 permitted = this.onMouse(event); 698 699 // Only unset targetElement if the click is not permitted. This will ensure that the check for !targetElement in onMouse fails and the browser's click doesn't go through. 700 if (!permitted) { 701 this.targetElement = null; 702 } 703 704 // If clicks are permitted, return true for the action to go through. 705 return permitted; 706 }; 707 708 709 /** 710 * Remove all FastClick's event listeners. 711 * 712 * @returns {void} 713 */ 714 FastClick.prototype.destroy = function() { 715 var layer = this.layer; 716 717 if (deviceIsAndroid) { 718 layer.removeEventListener('mouseover', this.onMouse, true); 719 layer.removeEventListener('mousedown', this.onMouse, true); 720 layer.removeEventListener('mouseup', this.onMouse, true); 721 } 722 723 layer.removeEventListener('click', this.onClick, true); 724 layer.removeEventListener('touchstart', this.onTouchStart, false); 725 layer.removeEventListener('touchmove', this.onTouchMove, false); 726 layer.removeEventListener('touchend', this.onTouchEnd, false); 727 layer.removeEventListener('touchcancel', this.onTouchCancel, false); 728 }; 729 730 731 /** 732 * Check whether FastClick is needed. 733 * 734 * @param {Element} layer The layer to listen on 735 */ 736 FastClick.notNeeded = function(layer) { 737 var metaViewport; 738 var chromeVersion; 739 var blackberryVersion; 740 var firefoxVersion; 741 742 // Devices that don't support touch don't need FastClick 743 if (typeof window.ontouchstart === 'undefined') { 744 return true; 745 } 746 747 // Chrome version - zero for other browsers 748 chromeVersion = +(/Chrome\/([0-9]+)/.exec(navigator.userAgent) || [,0])[1]; 749 750 if (chromeVersion) { 751 752 if (deviceIsAndroid) { 753 metaViewport = document.querySelector('meta[name=viewport]'); 754 755 if (metaViewport) { 756 // Chrome on Android with user-scalable="no" doesn't need FastClick (issue #89) 757 if (metaViewport.content.indexOf('user-scalable=no') !== -1) { 758 return true; 759 } 760 // Chrome 32 and above with width=device-width or less don't need FastClick 761 if (chromeVersion > 31 && document.documentElement.scrollWidth <= window.outerWidth) { 762 return true; 763 } 764 } 765 766 // Chrome desktop doesn't need FastClick (issue #15) 767 } else { 768 return true; 769 } 770 } 771 772 if (deviceIsBlackBerry10) { 773 blackberryVersion = navigator.userAgent.match(/Version\/([0-9]*)\.([0-9]*)/); 774 775 // BlackBerry 10.3+ does not require Fastclick library. 776 // https://github.com/ftlabs/fastclick/issues/251 777 if (blackberryVersion[1] >= 10 && blackberryVersion[2] >= 3) { 778 metaViewport = document.querySelector('meta[name=viewport]'); 779 780 if (metaViewport) { 781 // user-scalable=no eliminates click delay. 782 if (metaViewport.content.indexOf('user-scalable=no') !== -1) { 783 return true; 784 } 785 // width=device-width (or less than device-width) eliminates click delay. 786 if (document.documentElement.scrollWidth <= window.outerWidth) { 787 return true; 788 } 789 } 790 } 791 } 792 793 // IE10 with -ms-touch-action: none or manipulation, which disables double-tap-to-zoom (issue #97) 794 if (layer.style.msTouchAction === 'none' || layer.style.touchAction === 'manipulation') { 795 return true; 796 } 797 798 // Firefox version - zero for other browsers 799 firefoxVersion = +(/Firefox\/([0-9]+)/.exec(navigator.userAgent) || [,0])[1]; 800 801 if (firefoxVersion >= 27) { 802 // Firefox 27+ does not have tap delay if the content is not zoomable - https://bugzilla.mozilla.org/show_bug.cgi?id=922896 803 804 metaViewport = document.querySelector('meta[name=viewport]'); 805 if (metaViewport && (metaViewport.content.indexOf('user-scalable=no') !== -1 || document.documentElement.scrollWidth <= window.outerWidth)) { 806 return true; 807 } 808 } 809 810 // IE11: prefixed -ms-touch-action is no longer supported and it's recomended to use non-prefixed version 811 // http://msdn.microsoft.com/en-us/library/windows/apps/Hh767313.aspx 812 if (layer.style.touchAction === 'none' || layer.style.touchAction === 'manipulation') { 813 return true; 814 } 815 816 return false; 817 }; 818 819 820 /** 821 * Factory method for creating a FastClick object 822 * 823 * @param {Element} layer The layer to listen on 824 * @param {Object} [options={}] The options to override the defaults 825 */ 826 FastClick.attach = function(layer, options) { 827 return new FastClick(layer, options); 828 }; 829 830 831 if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) { 832 833 // AMD. Register as an anonymous module. 834 define(function() { 835 return FastClick; 836 }); 837 } else if (typeof module !== 'undefined' && module.exports) { 838 module.exports = FastClick.attach; 839 module.exports.FastClick = FastClick; 840 } else { 841 window.FastClick = FastClick; 842 } 843 }());
fastclick沒有什麼好註釋的,源代碼已經註釋的比較完善了。須要重點關注的是FastClick.prototype.onTouchEnd 函數,這個是核心函數。
1 FastClick.prototype.onTouchEnd = function(event) { 2 //這裏一堆定義 暫時不用關心 3 var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement; 4 5 //trackingClick會在touchstart中置爲true,這裏校驗是不是一個完整的touch事件 6 if (!this.trackingClick) { 7 return true; 8 } 9 10 //點擊過快 這次點擊無效 11 // Prevent phantom clicks on fast double-tap (issue #36) 12 if ((event.timeStamp - this.lastClickTime) < this.tapDelay) { 13 this.cancelNextClick = true; 14 return true; 15 } 16 17 //touchend與touchstart間隔過長,則再也不認爲這是一個click事件 18 if ((event.timeStamp - this.trackingClickStart) > this.tapTimeout) { 19 return true; 20 } 21 22 //一些重置操做 23 // Reset to prevent wrong click cancel on input (issue #156). 24 this.cancelNextClick = false; 25 26 this.lastClickTime = event.timeStamp; 27 28 trackingClickStart = this.trackingClickStart; 29 this.trackingClick = false; 30 this.trackingClickStart = 0; 31 32 // On some iOS devices, the targetElement supplied with the event is invalid if the layer 33 // is performing a transition or scroll, and has to be re-detected manually. Note that 34 // for this to function correctly, it must be called *after* the event target is checked! 35 // See issue #57; also filed as rdar://13048589 . 36 if (deviceIsIOSWithBadTarget) { 37 touch = event.changedTouches[0]; 38 39 // In certain cases arguments of elementFromPoint can be negative, so prevent setting targetElement to null 40 targetElement = document.elementFromPoint(touch.pageX - window.pageXOffset, touch.pageY - window.pageYOffset) || targetElement; 41 targetElement.fastClickScrollParent = this.targetElement.fastClickScrollParent; 42 } 43 44 targetTagName = targetElement.tagName.toLowerCase(); 45 if (targetTagName === 'label') { 46 forElement = this.findControl(targetElement); 47 if (forElement) { 48 this.focus(targetElement); 49 if (deviceIsAndroid) { 50 return false; 51 } 52 53 targetElement = forElement; 54 } 55 } else if (this.needsFocus(targetElement)) {//needsFocus:重點關注 發現這裏纔是咱們代碼不能好好工做的緣由 56 //touchend取消默認事件後,靠focus給input text焦點 57 // Case 1: If the touch started a while ago (best guess is 100ms based on tests for issue #36) then focus will be triggered anyway. Return early and unset the target element reference so that the subsequent click will be allowed through. 58 // Case 2: Without this exception for input elements tapped when the document is contained in an iframe, then any inputted text won't be visible even though the value attribute is updated as the user types (issue #37). 59 if ((event.timeStamp - trackingClickStart) > 100 || (deviceIsIOS && window.top !== window && targetTagName === 'input')) { 60 this.targetElement = null; 61 return false; 62 } 63 64 this.focus(targetElement); 65 this.sendClick(targetElement, event); 66 67 //這裏若不是IOS 阻止默認事件,但我用IOS9測試,IOS9也須要阻止默認事件。 68 // Select elements need the event to go through on iOS 4, otherwise the selector menu won't open. 69 // Also this breaks opening selects when VoiceOver is active on iOS6, iOS7 (and possibly others) 70 if (!deviceIsIOS || targetTagName !== 'select') { 71 this.targetElement = null; 72 event.preventDefault(); 73 } 74 75 //這個return 沒看到用處 76 return false; 77 } 78 79 if (deviceIsIOS && !deviceIsIOS4) { 80 81 // Don't send a synthetic click event if the target element is contained within a parent layer that was scrolled 82 // and this tap is being used to stop the scrolling (usually initiated by a fling - issue #42). 83 scrollParent = targetElement.fastClickScrollParent; 84 if (scrollParent && scrollParent.fastClickLastScrollTop !== scrollParent.scrollTop) { 85 return true; 86 } 87 } 88 89 // Prevent the actual click from going though - unless the target node is marked as requiring 90 // real clicks or if it is in the whitelist in which case only non-programmatic clicks are permitted. 91 if (!this.needsClick(targetElement)) { 92 event.preventDefault(); 93 this.sendClick(targetElement, event); 94 } 95 96 return false; 97 };
看到了fastclick的工做原理,修改咱們的代碼,最終以下:
1 <!doctype html> 2 <html> 3 <head> 4 <meta charset="utf-8"> 5 <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> 6 <title>手機端點擊</title> 7 <style> 8 body{margin:0;} 9 input{width:90%;height:20px;} 10 #demo1{padding-top:20px;} 11 #demo2{width:100%;background: #ebebe7;height:200px;position:absolute;top:0;text-align:center;padding-top:20px;} 12 #btn{background: #fff;height:25px;display:inline-block;color:red;opacity:1;} 13 .hide{display:none;} 14 </style> 15 </head> 16 <body> 17 <div id="demo1"> 18 <input id="text"> 19 </div> 20 <div id="demo2"> 21 <button id="btn">點擊我</button> 22 </div> 23 <script> 24 var demo1 = document.querySelector('#demo1'), demo2 = document.querySelector("#demo2"), btn = document.querySelector("#btn"), text = document.querySelector("#text") 25 26 document.addEventListener('click', function(e){ 27 if(e.ming){ 28 return true; 29 } 30 if (e.stopImmediatePropagation) { 31 e.stopImmediatePropagation(); 32 } else { 33 e.propagationStopped = true; 34 } 35 e.stopPropagation(); 36 e.preventDefault(); 37 return true; 38 }, true) 39 /*document.addEventListener('mousedown', function(e){ 40 if(e.ming){ 41 return true; 42 } 43 if (e.stopImmediatePropagation) { 44 e.stopImmediatePropagation(); 45 } else { 46 e.propagationStopped = true; 47 } 48 e.stopPropagation(); 49 e.preventDefault(); 50 return true; 51 }, true)*/ 52 53 text.addEventListener("click", function(){ 54 console.log("text click") 55 }) 56 57 text.addEventListener("touchend", function(){ 58 console.log("text touchend") 59 }) 60 61 text.addEventListener("touchstart", function(){ 62 console.log("text touchstart") 63 }) 64 text.addEventListener("mousedown", function(){ 65 console.log("text mousedown") 66 }) 67 68 btn.addEventListener('click', function(e){ 69 console.log(e.ming); 70 demo2.className = "hide"; 71 }) 72 73 var el 74 document.addEventListener("touchstart", function(e){ 75 el = e.target 76 }) 77 document.addEventListener("touchend", function(e){ 78 console.log('touchend') 79 var event = document.createEvent("MouseEvents") 80 event.initEvent("click", true, true) 81 event.ming = true 82 e.target.focus(); 83 el && el.dispatchEvent(event) 84 e.preventDefault(); 85 return true; 86 }) 87 </script> 88 </body> 89 </html>
正常工做,perfect