fastclick源码分析
在分析fastclcik源码之前需要先搞清楚为什么非得用click代替touchstart,移动端直接使用touchstart不就行了吗。我认为主要有以下两大理由:
1、部分网站PC端、移动端共用一套代码,都绑定了touchstart,PC端还怎么玩
2、二者触发条件不同:a)touchstart 手指触摸到显示屏即触发事件 b)click 手指触摸到显示屏,未曾在屏幕上移动(或移动一个非常小的位移值),然后手指离开屏幕,从触摸到离开时间间隔较短,此时才会触发click事件。
click体验要明显好于touchstart,故我们要为click填坑。
简单模拟
经过一段时间修改测试写下如下代码,运行效果还行:
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即可,闪烁暂时没找到方法避免,这不是本文重点,以后再研究。
点透问题
大家喜欢用fastclick还有个原因是它可以避免点透,现在先看看我们的代码能不能避免点透,先搞个例子:
<!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>
执行代码,顺利点透
将touchend默认事件阻止即可,这时就不会再出现点透问题,但悲剧的是input永远也获取不到焦点了
<!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被阻止了默认操作,怎么还会点透呢。测试如下:
<!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的解决方案。
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