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>
View Code

在用我的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>
View Code

执行代码,顺利点透

 将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>
View Code

此处分析:我们知道点击后事件顺序如下: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>
View Code

结果发现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>
View Code

手机测试发现,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 }());
View Code

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>
View Code

正常工作,perfect

posted on 2016-11-23 23:13  风吹树叶黄  阅读(408)  评论(0编辑  收藏  举报

导航