FastClick 源码解析

faskclick是为了消除在移动端上用户的点击后有300-350ms延迟,这是因为在移动端浏览器为了确认当前用户是否为双击。300-350ms的延迟是在touchend与click之间。

使用方式如下:

var FastClick = require('fastclick')
FastClick.attach(document.body)

一、引用时得到的什么?

if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) {
    // AMD. Register as an anonymous module.
    define(function() {
        return FastClick;
    });
} else if (typeof module !== 'undefined' && module.exports) {
    module.exports = FastClick.attach;
    module.exports.FastClick = FastClick;
} else {
    window.FastClick = FastClick;
}

这里兼容了三种方式:AMD、CMD或直接赋值在window上;不管用哪种方式引用,最终拿到的是FastClick

二、FastClick.attach做了什么?

FastClick.attach = function(layer, options) {
    return new FastClick(layer, options);
};

它是FastClick方法的静态方法,调用它就是创建了一个FaskClick实例。

三、FastClick实例初始化了什么?

function FastClick(layer, options) {
    // ...
}

FastClick的构造函数共分为6部分:

1、初始了一系列属性;

var oldOnClick;
options = options || {};
// Whether a click is currently being tracked.
this.trackingClick = false;
// Timestamp for when click tracking started.
this.trackingClickStart = 0;
// The element being tracked for a click.
this.targetElement = null;
// X-coordinate of touch start event.
this.touchStartX = 0;
// Y-coordinate of touch start event.
this.touchStartY = 0;
// ID of the last touch, retrieved from Touch.identifier.
this.lastTouchIdentifier = 0;
// Touchmove boundary, beyond which a click will be cancelled.
this.touchBoundary = options.touchBoundary || 10;
// The FastClick layer.
this.layer = layer;
// The minimum time between tap(touchstart and touchend) events
this.tapDelay = options.tapDelay || 200;
// The maximum time for a tap
this.tapTimeout = options.tapTimeout || 700;

2、调用FastClick的静态函数notNeeded判断当前环境是否支持,如果不支持则返回true,否则返回false;

if (FastClick.notNeeded(layer)) {
    return;
}

FastClick.notNeeded方法主要针对如下几种情况视为无需使用FastClick,具体如下:
1、如果设备不支持touch的

// Devices that don't support touch don't need FastClick
if (typeof window.ontouchstart === 'undefined') {
    return true;
}

2、针对使用Chrome浏览器的设备,一些几种情况不加FastClick:
(1)在安卓设备上,Chrome发布的第一个版本就删除了touchend与click之间的延时,虽然这也使得页面不可缩放,但是从Chrome 32开始,消除了touchend与click之间的延时同时也支持页面可缩放。随后Firefox与IE/Edge也做了相同的设置,2016年3月,IOS 9.3也是;
只需要页面的head中设置meta标签user-scalable="no"或是在Chrome 32及以上设置width=device-with即可自动禁用300-350ms的延时,无需再FastClick
(2)在其他设备上,即桌面设备,桌面设备更不需要
在Windows 7及以上的可触摸设备上,使用Chrome打开window.ontouchstart != undefined,这些设备可能会有接入外部的鼠标或键盘,可以点击或是触摸的,在这些设备上添加触摸事件的监听会导致页面不可点击

// Chrome version - zero for other browsers
chromeVersion = +(/Chrome\/([0-9]+)/.exec(navigator.userAgent) || [,0])[1];
if (chromeVersion) {
    if (deviceIsAndroid) {
        metaViewport = document.querySelector('meta[name=viewport]');
        if (metaViewport) {
            // Chrome on Android with user-scalable="no" doesn't need FastClick (issue #89)
            if (metaViewport.content.indexOf('user-scalable=no') !== -1) {
                return true;
            }
            // Chrome 32 and above with width=device-width or less don't need FastClick
            if (chromeVersion > 31 && document.documentElement.scrollWidth <= window.outerWidth) {
                return true;
            }
        }

    // Chrome desktop doesn't need FastClick (issue #15)
    } else {
        return true;
    }
}

3、BlackBerry手机10.3+已经不再有300ms的延时,因此无需处理

if (deviceIsBlackBerry10) {
    blackberryVersion = navigator.userAgent.match(/Version\/([0-9]*)\.([0-9]*)/);
    // BlackBerry 10.3+ does not require Fastclick library.
    // https://github.com/ftlabs/fastclick/issues/251
    if (blackberryVersion[1] >= 10 && blackberryVersion[2] >= 3) {
        metaViewport = document.querySelector('meta[name=viewport]');
        if (metaViewport) {
            // user-scalable=no eliminates click delay.
            if (metaViewport.content.indexOf('user-scalable=no') !== -1) {
                return true;
            }
            // width=device-width (or less than device-width) eliminates click delay.
            if (document.documentElement.scrollWidth <= window.outerWidth) {
                return true;
            }
        }
    }
}

4、在IE10上,如果设置了-ms-touch-action: none/manipulation,就会禁止放大功能,因为此时也无需处理延时问题;Firefox 27+ 如果在viewport标签上已经设置了user-scalable=no或width=device-width,表示禁止缩放,此时也是不存在延时的;其他情况设置-ms-touch-action: none/manipulation也是需要处理延时问题。

// IE10 with -ms-touch-action: none or manipulation, which disables double-tap-to-zoom (issue #97)
if (layer.style.msTouchAction === 'none' || layer.style.touchAction === 'manipulation') {
    return true;
}
// Firefox version - zero for other browsers
firefoxVersion = +(/Firefox\/([0-9]+)/.exec(navigator.userAgent) || [,0])[1];
if (firefoxVersion >= 27) {
    // Firefox 27+ does not have tap delay if the content is not zoomable - https://bugzilla.mozilla.org/show_bug.cgi?id=922896
    metaViewport = document.querySelector('meta[name=viewport]');
    if (metaViewport && (metaViewport.content.indexOf('user-scalable=no') !== -1 || document.documentElement.scrollWidth <= window.outerWidth)) {
        return true;
    }
}
// IE11: prefixed -ms-touch-action is no longer supported and it's recomended to use non-prefixed version
// http://msdn.microsoft.com/en-us/library/windows/apps/Hh767313.aspx
if (layer.style.touchAction === 'none' || layer.style.touchAction === 'manipulation') {
    return true;
}
return false;

3、给当前的实例的一系列事件['onMouse', 'onClick', 'onTouchStart', 'onTouchMove', 'onTouchEnd', 'onTouchCancel']绑定this的指向,指向当前的实例;

// Some old versions of Android don't have Function.prototype.bind
function bind(method, context) {
    return function() { return method.apply(context, arguments); };
}

var methods = ['onMouse', 'onClick', 'onTouchStart', 'onTouchMove', 'onTouchEnd', 'onTouchCancel'];
var context = this;
for (var i = 0, l = methods.length; i < l; i++) {
    context[methods[i]] = bind(context[methods[i]], context);
}

4、给当前的layer即我们的document.body绑定事件:click、touchstart、touchmove、touchend、touchcancel,如果是安卓手机,还会绑定mouseover、mousedown、mouseup事件,

这里注意,click、mouseover、mousedown、mouseup使用捕获的方式,其他使用冒泡的方式

// Set up event handlers as required
if (deviceIsAndroid) {
    layer.addEventListener('mouseover', this.onMouse, true);
    layer.addEventListener('mousedown', this.onMouse, true);
    layer.addEventListener('mouseup', this.onMouse, true);
}

layer.addEventListener('click', this.onClick, true);
layer.addEventListener('touchstart', this.onTouchStart, false);
layer.addEventListener('touchmove', this.onTouchMove, false);
layer.addEventListener('touchend', this.onTouchEnd, false);
layer.addEventListener('touchcancel', this.onTouchCancel, false);

5、判断是否存在stopImmediatePropagation,它主要是用来阻止冒泡和触发用户声明的事件监听回调,如果不存在,则重定义layer.removeEventListenerlayer.addEventListener,主要是兼容一些不支持stopImmediatePropagation的情况。

// Hack is required for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2)
// which is how FastClick normally stops click events bubbling to callbacks registered on the FastClick
// layer when they are cancelled.
if (!Event.prototype.stopImmediatePropagation) {
    layer.removeEventListener = function(type, callback, capture) {
        var rmv = Node.prototype.removeEventListener;
        if (type === 'click') {
            rmv.call(layer, type, callback.hijacked || callback, capture);
        } else {
            rmv.call(layer, type, callback, capture);
        }
    };

    layer.addEventListener = function(type, callback, capture) {
        var adv = Node.prototype.addEventListener;
        if (type === 'click') {
            adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) {
                if (!event.propagationStopped) {
                    callback(event);
                }
            }), capture);
        } else {
            adv.call(layer, type, callback, capture);
        }
    };
}

目前只在onMouse事件上调用了stopImmediatePropagation方法,它主要就是在事件类型为click且未阻止事件冒泡时才调用click事件的回调。

// Derive and check the target element to see whether the mouse event needs to be permitted;
// unless explicitly enabled, prevent non-touch click events from triggering actions,
// to prevent ghost/doubleclicks.
if (!this.needsClick(this.targetElement) || this.cancelNextClick) {
    // Prevent any user-added listeners declared on FastClick element from being fired.
    if (event.stopImmediatePropagation) {
        event.stopImmediatePropagation();
    } else {
        // Part of the hack for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2)
        event.propagationStopped = true;
    }
    // Cancel the event
    event.stopPropagation();
    event.preventDefault();

    return false;
}

6、判断当前用户是否有在layer上面直接定义onclick事件,如果有,将其转换为addEventListener的方式定义,它会在Fastclick的onClick回调之后触发;

// If a handler is already declared in the element's onclick attribute, it will be fired before
// FastClick's onClick handler. Fix this by pulling out the user-defined handler function and
// adding it as listener.
if (typeof layer.onclick === 'function') {
    // Android browser on at least 3.2 requires a new reference to the function in layer.onclick
    // - the old one won't work if passed to addEventListener directly.
    oldOnClick = layer.onclick;
    layer.addEventListener('click', function(event) {
        oldOnClick(event);
    }, false);
    layer.onclick = null;
}

四、FastClick在事件触发时做了什么?

这里我们只看touchstart、touchmove、touchend、click,按照事件执行顺序挨个分析。

1、touchstart

layer.addEventListener('touchstart', this.onTouchStart, false);

它调用了onTouchStart函数:

1、忽略多个手指操作页面时,如缩放
// Ignore multiple touches, otherwise pinch-to-zoom is prevented if both fingers are on the FastClick element (issue #111).
if (event.targetTouches.length > 1) {
    return true;
}
2、获取当前事件点击的节点,如果当前节点是文本,则取其父节点
FastClick.prototype.getTargetElementFromEventTarget = function(eventTarget) {
    // On some older browsers (notably Safari on iOS 4.1 - see issue #56) the event target may be a text node.
    if (eventTarget.nodeType === Node.TEXT_NODE) {
        return eventTarget.parentNode;
    }
    return eventTarget;
};
var targetElement, touch, selection;
targetElement = this.getTargetElementFromEventTarget(event.target);
touch = event.targetTouches[0];
3、如果在IOS上,有选择内容,则忽略;在IOS非4版本上,只有在当前touch.identifier与上一次的相等时才认为是页面alert或confirm之后用户下一次点击页面上的任何其他地方时触发的,此时阻止默认事件,并返回
if (deviceIsIOS) {
    // Only trusted events will deselect text on iOS (issue #49)
    selection = window.getSelection();
    if (selection.rangeCount && !selection.isCollapsed) {
        return true;
    }
    if (!deviceIsIOS4) {
        // Weird things happen on iOS when an alert or confirm dialog is opened from a click event callback (issue #23):
        // when the user next taps anywhere else on the page, new touchstart and touchend events are dispatched
        // with the same identifier as the touch event that previously triggered the click that triggered the alert.
        // Sadly, there is an issue on iOS 4 that causes some normal touch events to have the same identifier as an
        // immediately preceeding touch event (issue #52), so this fix is unavailable on that platform.
        // Issue 120: touch.identifier is 0 when Chrome dev tools 'Emulate touch events' is set with an iOS device UA string,
        // which causes all touch events to be ignored. As this block only applies to iOS, and iOS identifiers are always long,
        // random integers, it's safe to to continue if the identifier is 0 here.
        if (touch.identifier && touch.identifier === this.lastTouchIdentifier) {
            event.preventDefault();
            return false;
        }
        this.lastTouchIdentifier = touch.identifier;

        // If the target element is a child of a scrollable layer (using -webkit-overflow-scrolling: touch) and:
        // 1) the user does a fling scroll on the scrollable layer
        // 2) the user stops the fling scroll with another tap
        // then the event.target of the last 'touchend' event will be the element that was under the user's finger
        // when the fling scroll was started, causing FastClick to send a click event to that layer - unless a check
        // is made to ensure that a parent layer was not scrolled before sending a synthetic click (issue #42).
        this.updateScrollParent(targetElement);
    }
}

这里的updateScrollParent主要用于记录有滚动的父节点与其scrollTop

4、初始化属性
this.trackingClick = true;
this.trackingClickStart = event.timeStamp;
this.targetElement = targetElement;
this.touchStartX = touch.pageX;
this.touchStartY = touch.pageY;
// Prevent phantom clicks on fast double-tap (issue #36)
if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {
    event.preventDefault();
}
return true

2、touchmove

layer.addEventListener('touchmove', this.onTouchMove, false);

它调用了onTouchMove函数:

FastClick.prototype.onTouchMove = function(event) {
    if (!this.trackingClick) {
        return true;
    }
    // If the touch has moved, cancel the click tracking
    if (this.targetElement !== this.getTargetElementFromEventTarget(event.target) || this.touchHasMoved(event)) {
        this.trackingClick = false;
        this.targetElement = null;
    }

    return true;
};

这里如果没有执行到touchstart最后的初始化属性,则忽略;如果当前触发事件的节点与记录的触发事件的节点不是一个,则设置trackingClick = false,targetElement = null

3、touchend

layer.addEventListener('touchend', this.onTouchEnd, false);

它调用了onTouchEnd事件:

1、如果trackingClick=false,则忽略
var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement;
if (!this.trackingClick) {
    return true;
}
2、如果事件与上次记录的事件时间小于200ms或大于700ms,则忽略
// Prevent phantom clicks on fast double-tap (issue #36)
if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {
    this.cancelNextClick = true;
    return true;
}
if ((event.timeStamp - this.trackingClickStart) > this.tapTimeout) {
    return true;
}
3、设置属性
// Reset to prevent wrong click cancel on input (issue #156).
this.cancelNextClick = false;

this.lastClickTime = event.timeStamp;

trackingClickStart = this.trackingClickStart;
this.trackingClick = false;
this.trackingClickStart = 0;
4、在IOS 6、7版本上执行transition或scroll时,拿到的targetElement无效,此时获取当前文档上处于指定坐标位置最顶层的元素, 坐标是相对于包含该文档的浏览器窗口的左上角为原点来计算的。
// On some iOS devices, the targetElement supplied with the event is invalid if the layer
// is performing a transition or scroll, and has to be re-detected manually. Note that
// for this to function correctly, it must be called *after* the event target is checked!
// See issue #57; also filed as rdar://13048589 .
if (deviceIsIOSWithBadTarget) {
    touch = event.changedTouches[0];
    // In certain cases arguments of elementFromPoint can be negative, so prevent setting targetElement to null
    targetElement = document.elementFromPoint(touch.pageX - window.pageXOffset, touch.pageY - window.pageYOffset) || targetElement;
    targetElement.fastClickScrollParent = this.targetElement.fastClickScrollParent;
}
5、如果当前触发事件的元素标签是label,则获取label标签下的button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea等元素,存在则获取焦点;否则判断当前元素是否需要获取焦点,即textareainput标签type为非button、checkbox、file、image、radio、submit且非disabled非readOnly的其他类型时,获取焦点,同时发送click事件到具体的元素。最后返回
targetTagName = targetElement.tagName.toLowerCase();
if (targetTagName === 'label') {
    forElement = this.findControl(targetElement);
    if (forElement) {
        this.focus(targetElement);
        if (deviceIsAndroid) {
            return false;
        }

        targetElement = forElement;
    }
} else if (this.needsFocus(targetElement)) {

    // 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.
    // 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).
    if ((event.timeStamp - trackingClickStart) > 100 || (deviceIsIOS && window.top !== window && targetTagName === 'input')) {
        this.targetElement = null;
        return false;
    }

    this.focus(targetElement);
    this.sendClick(targetElement, event);

    // Select elements need the event to go through on iOS 4, otherwise the selector menu won't open.
    // Also this breaks opening selects when VoiceOver is active on iOS6, iOS7 (and possibly others)
    if (!deviceIsIOS || targetTagName !== 'select') {
        this.targetElement = null;
        event.preventDefault();
    }

    return false;
}
5、最后,IOS且非4版本如果在滚动,则忽略,其他情况判断在不需要点击的情况,阻止其默认行为,同时发送点击事件,返回
if (deviceIsIOS && !deviceIsIOS4) {
    // Don't send a synthetic click event if the target element is contained within a parent layer that was scrolled
    // and this tap is being used to stop the scrolling (usually initiated by a fling - issue #42).
    scrollParent = targetElement.fastClickScrollParent;
    if (scrollParent && scrollParent.fastClickLastScrollTop !== scrollParent.scrollTop) {
        return true;
    }
}
// Prevent the actual click from going though - unless the target node is marked as requiring
// real clicks or if it is in the whitelist in which case only non-programmatic clicks are permitted.
if (!this.needsClick(targetElement)) {
    event.preventDefault();
    this.sendClick(targetElement, event);
}
return false;
6、这里我们在看一下sendClick做了什么?
FastClick.prototype.sendClick = function(targetElement, event) {
    var clickEvent, touch;
    // On some Android devices activeElement needs to be blurred otherwise the synthetic click will have no effect (#24)
    if (document.activeElement && document.activeElement !== targetElement) {
        document.activeElement.blur();
    }
    touch = event.changedTouches[0];
    // Synthesise a click event, with an extra attribute so it can be tracked
    clickEvent = document.createEvent('MouseEvents');
    clickEvent.initMouseEvent(this.determineEventType(targetElement), true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);
    clickEvent.forwardedTouchEvent = true;
    targetElement.dispatchEvent(clickEvent);
};
FastClick.prototype.determineEventType = function(targetElement) {
    //Issue #159: Android Chrome Select Box does not open with a synthetic click event
    if (deviceIsAndroid && targetElement.tagName.toLowerCase() === 'select') {
        return 'mousedown';
    }
    return 'click';
};

它主要是创建事件,并触发。

4、touchcancel

layer.addEventListener('touchcancel', this.onTouchCancel, false);

它的内容比较简单,如下:

FastClick.prototype.onTouchCancel = function() {
    this.trackingClick = false;
    this.targetElement = null;
};

5、click

layer.addEventListener('click', this.onClick, true);

它调用onClick函数:

FastClick.prototype.onClick = function(event) {
    var permitted;
    // 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.
    if (this.trackingClick) {
        this.targetElement = null;
        this.trackingClick = false;
        return true;
    }
    // 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.
    if (event.target.type === 'submit' && event.detail === 0) {
        return true;
    }
    permitted = this.onMouse(event);
    // 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.
    if (!permitted) {
        this.targetElement = null;
    }
    // If clicks are permitted, return true for the action to go through.
    return permitted;
};

五、总结

1、可以看到FastClick主要是针对怪异现象做hack处理。
2、在事件监听回调中,我们看到很多return false与return true,其实是无意义的,只是阻止了程序往下执行而已;
可能有些同学认为return false是为了阻止其默认行为,根据HTML5 Section 6.1.5.1 of the HTML Spec规范,事件处理程序的返回值只对通过属性注册的处理程序才有意义,如果我们未通过addEventListener()函数来绑定事件的话,若要禁止默认事件,用的就是return false; 但如果要用addEventListener()或者attachEvent()来绑定,就要用preventDefault()方法或者设置事件对象的returnValue属性;在规范中有指出在mouseover等几种特殊事件情况下,return false;并不一定能终止事件。所以,在实际使用中,我们需要尽量避免通过return false;的方式来取消事件的默认行为;所以在FastClick使用addEventListener监听事件,return true/false没有什么特殊的作用。
3、它消除了移动端上用户点击后有300-350ms的延时,主要是在touchend时通过自定义click事件并触发实现的。

六、遇到的坑

1、加入了FastClick,在IOS上点击input标签会发现有时很难聚焦

在我们处理touchend监听的时候也处理focus事件:

if (this.needsFocus(targetElement)) {
    // 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.
    // 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).
    if ((event.timeStamp - trackingClickStart) > 100 || (deviceIsIOS && window.top !== window && targetTagName === 'input')) {
        this.targetElement = null;
        return false;
    }
    this.focus(targetElement);
    this.sendClick(targetElement, event);
    // Select elements need the event to go through on iOS 4, otherwise the selector menu won't open.
    // Also this breaks opening selects when VoiceOver is active on iOS6, iOS7 (and possibly others)
    if (!deviceIsIOS || targetTagName !== 'select') {
        this.targetElement = null;
        event.preventDefault();
    }
    return false;
}

我们的input事件之所以不能聚焦,是因为在focus后面判断
targetTagName !== 'select'时调用event.preventDefault()阻止了默认行为;虽然我们去掉这里阻止的默认行为可以解决,但同样的,有这段代码也是为了针对部分手机的怪异行为做的hack处理,所以我们看看focus方法有没有可以优化的地方。focus方法内容如下:

FastClick.prototype.focus = function(targetElement) {
    var length;
    // 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.
    if (deviceIsIOS && targetElement.setSelectionRange && targetElement.type.indexOf('date') !== 0 && targetElement.type !== 'time' && targetElement.type !== 'month') {
        length = targetElement.value.length;
        targetElement.setSelectionRange(length, length);
    } else {
        targetElement.focus();
    }
};

在IOS 7上一些input标签type为date | time | month的时候在设置选中的范围时通常会报错,这里针对input其他类型时使用setSelectionRange来设定光标,同时将光标定在输入框的最后。这里我们并没有做任何的聚焦处理,所以使用了默认行为,但由于后面我们阻止了默认行为,所以针对这种情况我们也应该做手动聚焦处理,将focus方法修改为:

FastClick.prototype.focus = function(targetElement) {
    var length;
    if (deviceIsIOS && targetElement.setSelectionRange && targetElement.type.indexOf('date') !== 0 && targetElement.type !== 'time' && targetElement.type !== 'month') {
        length = targetElement.value.length;
        targetElement.setSelectionRange(length, length);
        targetElement.focus();
    } else {
        targetElement.focus();
    }
};

这里由于我们调用了setSelectionRange(length, length),点击的时候光标始终会放在文末,如果不需要也可直接去掉:

FastClick.prototype.focus = function(targetElement) {
    targetElement.focus();
};

参考文档:
https://www.cnblogs.com/momo7...
https://blog.csdn.net/a460550...
知识点:
https://developers.google.com...
https://developer.mozilla.org...
https://developer.mozilla.org...
https://developer.mozilla.org...
https://developer.mozilla.org...
https://developer.mozilla.org...
https://developer.mozilla.org...
https://developer.mozilla.org...
https://developer.mozilla.org...

有兴趣可关注公众号
FastClick 源码解析_第1张图片

你可能感兴趣的:(fastclick,源码分析,前端)