fastclick源碼分析

在分析fastclcik源碼以前須要先搞清楚爲何非得用click代替touchstart,移動端直接使用touchstart不就好了嗎。我認爲主要有如下兩大理由:javascript

一、部分網站PC端、移動端共用一套代碼,都綁定了touchstart,PC端還怎麼玩css

二、兩者觸發條件不一樣:a)touchstart 手指觸摸到顯示屏即觸發事件 b)click 手指觸摸到顯示屏,不曾在屏幕上移動(或移動一個很是小的位移值),而後手指離開屏幕,從觸摸到離開時間間隔較短,此時纔會觸發click事件。html

click體驗要明顯好於touchstart,故咱們要爲click填坑。html5

簡單模擬

 通過一段時間修改測試寫下以下代碼,運行效果還行:java

 1 <!doctype html>
 2 <html>
 3 <head>
 4     <meta charset="utf-8">
 5     <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimun-scale=1.0">
 6     <title>手機端點擊</title>
 7 </head>
 8 <body>
 9 <div id="demo1" style="width:100px;height:100px;background:red;"></div>
10 <div id="demo2" style="width:100px;height:100px;background:blue;"></div>
11 <script>
12     var demo1 = document.querySelector('#demo1'), demo2 = document.querySelector("#demo2")
13     demo1.addEventListener('touchstart', function(){
14         demo1.innerHTML=demo1.innerHTML + "<br>touchstart"
15     })
16     demo2.addEventListener('click', function (e) {
17         if(!e.ming){
18             e.preventDefault();
19             return
20         }
21         demo2.innerHTML=demo2.innerHTML + "<br>click"
22     })
23 
24     var el
25     document.addEventListener("touchstart", function(e){
26         el = e.target
27     })
28     document.addEventListener("touchend", function(e){
29         var event = document.createEvent("MouseEvents")
30         event.initEvent("click", true, true)
31         event.ming = true
32         el && el.dispatchEvent(event)
33     })
34 </script>
35 </body>
36 </html>
View Code

在用個人IOS9瀏覽器測試時,效果還行。就是點擊demo2時會有閃爍發生,並有一個黑框。通過測試,黑框是outline,閃爍是click觸發了瀏覽器的重繪。outline設爲none便可,閃爍暫時沒找到方法避免,這不是本文重點,之後再研究。node

點透問題

你們喜歡用fastclick還有個緣由是它能夠避免點透,如今先看看咱們的代碼能不能避免點透,先搞個例子:android

<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimun-scale=1.0">
    <title>手機端點擊</title>
<style>
body{margin:0;}
input{width:100%;height:20px;}
#demo1{padding-top:20px;}
#demo2{width:100%;background: #ebebe7;height:200px;position:absolute;top:0;text-align:center;padding-top:20px;}
#btn{background: #fff;height:25px;display:inline-block;color:red;opacity:1;}
.hide{display:none;}
</style>
</head>
<body>
<div id="demo1">
    <input id="text">
</div>
<div id="demo2">
    <button id="btn">點擊我</button>
</div>
<script>
    var demo1 = document.querySelector('#demo1'), demo2 = document.querySelector("#demo2"), btn = document.querySelector("#btn")

    btn.addEventListener('click', function(e){
        if(!e.ming){
            e.preventDefault();
            e.stopPropagation();
            return
        }
        demo2.className = "hide";
    })

    var el
    document.addEventListener("touchstart", function(e){
        el = e.target
    })
    document.addEventListener("touchend", function(e){
        var event = document.createEvent("MouseEvents")
        event.initEvent("click", true, true)
        event.ming = true
        el && el.dispatchEvent(event)
    })
</script>
</body>
</html>
View Code

執行代碼,順利點透git

 將touchend默認事件阻止便可,這時就不會再出現點透問題,但悲劇的是input永遠也獲取不到焦點了github

<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <title>手機端點擊</title>
<style>
body{margin:0;}
input{width:90%;height:20px;}
#demo1{padding-top:20px;}
#demo2{width:100%;background: #ebebe7;height:200px;position:absolute;top:0;text-align:center;padding-top:20px;}
#btn{background: #fff;height:25px;display:inline-block;color:red;opacity:1;}
.hide{display:none;}
</style>
</head>
<body>
<div id="demo1">
    <input id="text">
</div>
<div id="demo2">
    <button id="btn">點擊我</button>
</div>
<script>
    var demo1 = document.querySelector('#demo1'), demo2 = document.querySelector("#demo2"), btn = document.querySelector("#btn")

    document.addEventListener('click', function(e){
        if(e.ming){
            return true;
        }
        if (e.stopImmediatePropagation) {
            e.stopImmediatePropagation();
        } else {
            e.propagationStopped = true;
        }
        e.stopPropagation();
        e.preventDefault();
        return true;
    }, true)

    btn.addEventListener('click', function(e){
        demo2.className = "hide";
    })

    var el
    document.addEventListener("touchstart", function(e){
        el = e.target
    })
    document.addEventListener("touchend", function(e){
        e.preventDefault();
        var event = document.createEvent("MouseEvents")
        event.initEvent("click", true, true)
        event.ming = true
        el && el.dispatchEvent(event)
    })
</script>
</body>
</html>
View Code

此處分析:咱們知道點擊後事件順序以下:touchstart、touchend、click,touchend觸發後懸浮框demo2隱藏,瀏覽器自帶的click被阻止了默認操做,怎麼還會點透呢。測試以下:web

<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <title>手機端點擊</title>
<style>
body{margin:0;}
input{width:90%;height:20px;}
#demo1{padding-top:20px;}
#demo2{width:100%;background: #ebebe7;height:200px;position:absolute;top:0;text-align:center;padding-top:20px;}
#btn{background: #fff;height:25px;display:inline-block;color:red;opacity:1;}
.hide{display:none;}
</style>
</head>
<body>
<div id="demo1">
    <input id="text">
</div>
<div id="demo2">
    <button id="btn">點擊我</button>
</div>
<script>
    var demo1 = document.querySelector('#demo1'), demo2 = document.querySelector("#demo2"), btn = document.querySelector("#btn"), text = document.querySelector("#text")

    document.addEventListener('click', function(e){
        if(e.ming){
            return true;
        }
        if (e.stopImmediatePropagation) {
            e.stopImmediatePropagation();
        } else {
            e.propagationStopped = true;
        }
        e.stopPropagation();
        e.preventDefault();
        return true;
    }, true)
    /*document.addEventListener('mousedown', function(e){
        if(e.ming){
            return true;
        }
        if (e.stopImmediatePropagation) {
            e.stopImmediatePropagation();
        } else {
            e.propagationStopped = true;
        }
        e.stopPropagation();
        e.preventDefault();
        return true;
    }, true)*/

    text.addEventListener("click", function(){
        console.log("text click")
    })

    text.addEventListener("touchend", function(){
        console.log("text touchend")
    })

    text.addEventListener("touchstart", function(){
        console.log("text touchstart")
    })
    text.addEventListener("mousedown", function(){
        console.log("text mousedown")
    })

    btn.addEventListener('click', function(e){
        console.log(e.ming);
        demo2.className = "hide";
    })

    var el
    document.addEventListener("touchstart", function(e){
        el = e.target
    })
    document.addEventListener("touchend", function(e){
        console.log('touchend')
        var event = document.createEvent("MouseEvents")
        event.initEvent("click", true, true)
        event.ming = true
        el && el.dispatchEvent(event)
    })
</script>
</body>
</html>
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

相關文章
相關標籤/搜索