• 移动端click事件延迟300ms的原因以及解决办法


    这要追溯至 2007 年初。苹果公司在发布首款 iPhone 前夕,遇到一个问题 —— 当时的网站都是为大屏幕设备所设计的。于是苹果的工程师们做了一些约定,应对 iPhone 这种小屏幕浏览桌面端站点的问题。这当中最出名的,当属双击缩放(double tap to zoom)。这也是会有上述 300 毫秒延迟的主要原因。

    当用户一次点击屏幕之后,浏览器并不能立刻判断用户是要进行双击缩放,还是想要进行单击操作。因此,iOS Safari 就等待 300 毫秒,以判断用户是否再次点击了屏幕。

    于是,300 毫秒延迟就这么诞生了。

    FastClick 是 FT Labs 专门为解决移动端浏览器 300 毫秒点击延迟问题所开发的一个轻量级的库。简而言之,FastClick 在检测到 touchend 事件的时候,会通过 DOM 自定义事件立即触发一个模拟click 事件的click事件(自定义事件),并把浏览器在 300 毫秒之后真正触发的 click 事件阻止掉。

    FastClick 非常实际地解决 300 毫秒点击延迟的问题。唯一的缺点可能也就是该脚本的文件尺寸 (尽管它不大)。如果你非常在意这点文件大小,可以尝试一下 Filament Group 的 Tappy!,或者 tap.js。两者都相当轻量,能够通过监听 tap 而非 click 事件来绕过 300 毫秒延迟。

    当然,zepto库函数中,也有一个touch模块,此模块也包含tap事件,可以通过绑定tap来取代click事件。

    但是zepto的tap事件会有点透问题。如何解决,请看下篇分解。

    接下来,我们来详细了解一个问题:FastClick解决延迟点击的源码解析。

    FastClick解决延迟点击的源码解析。

    首先,我们来看FastClick的使用。

    window.addEventListener( "load", function() {
        FastClick.attach( document.body );
    }, false );
    

    这样就解决了 300 毫秒点击延迟的问题。

    FastClick的源码:

    FastClick.attach = function(layer) {
    	'use strict';
    	return new FastClick(layer);
    };
    

    在FastClick的构造函数中,会有下面这段代码:

    	this.onClick = function() { return FastClick.prototype.onClick.apply(self, arguments); };
    
    	this.onMouse = function() { return FastClick.prototype.onMouse.apply(self, arguments); };
    
    	this.onTouchStart = function() { return FastClick.prototype.onTouchStart.apply(self, arguments); };
    
    	this.onTouchEnd = function() { return FastClick.prototype.onTouchEnd.apply(self, arguments); };
    
    	this.onTouchCancel = function() { return FastClick.prototype.onTouchCancel.apply(self, arguments); };
    
    	if (FastClick.notNeeded(layer)) {
    		return;
    	}
    	if (this.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('touchend', this.onTouchEnd, false);
    	layer.addEventListener('touchcancel', this.onTouchCancel, false);
    

    也就是在document.body上绑定了click,touchstart,touchend,touchcancel事件。

    这里假设,我们的页面有一个button,绑定了click事件。

    当用户点击此button时,会先触发touchstart事件,这时,会冒泡到document.body中,于是就会执行:

    FastClick.prototype.onTouchStart = function(event) {
    	'use strict';
    	var targetElement, touch, selection;
    
    	if (event.targetTouches.length > 1) {
    		return true;
    	}
    	targetElement = this.getTargetElementFromEventTarget(event.target);
    	touch = event.targetTouches[0];
    
    	if (this.deviceIsIOS) {
    
    		selection = window.getSelection();
    		if (selection.rangeCount && !selection.isCollapsed) {
    			return true;
    		}
    
    		if (!this.deviceIsIOS4) {
    
    			if (touch.identifier === this.lastTouchIdentifier) {
    				event.preventDefault();
    				return false;
    			}
    
    			this.lastTouchIdentifier = touch.identifier;	
    			this.updateScrollParent(targetElement);
    		}
    	}
    	this.trackingClick = true;
    	this.trackingClickStart = event.timeStamp;
    	this.targetElement = targetElement;
    	this.touchStartX = touch.pageX;
    	this.touchStartY = touch.pageY;
    	if ((event.timeStamp - this.lastClickTime) < 200) {
    		event.preventDefault();
    	}
    	return true;
    };
    

    这个回调函数主要做了以下事情:

    获取我们当前触发touchstart的元素,这里是button。

    然后将鼠标的信息记录了下来,记录鼠标的信息主要是为了在后面touchend触发时,根据这里得到的x、y判断是否为click。

    之后,会触发touchend事件,然后冒泡到document.body上,执行以下代码:

    FastClick.prototype.onTouchEnd = function(event) {
    	'use strict';
    	var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement;
    	if (this.touchHasMoved(event) || (event.timeStamp - this.trackingClickStart) > 300) {
    		this.trackingClick = false;
    		this.targetElement = null;
    	}
    	if (!this.trackingClick) {
    		return true;
    	}
    	if ((event.timeStamp - this.lastClickTime) < 200) {
    		this.cancelNextClick = true;
    		return true;
    	}
    
    	this.lastClickTime = event.timeStamp;
    	trackingClickStart = this.trackingClickStart;
    	this.trackingClick = false;
    	this.trackingClickStart = 0;
    	if (this.deviceIsIOSWithBadTarget) {
    		touch = event.changedTouches[0];
    		targetElement = document.elementFromPoint(touch.pageX - window.pageXOffset, touch.pageY - window.pageYOffset);
    	}
    	targetTagName = targetElement.tagName.toLowerCase();
    	if (targetTagName === 'label') {
    		forElement = this.findControl(targetElement);
    		if (forElement) {
    			this.focus(targetElement);
    			if (this.deviceIsAndroid) {
    				return false;
    			}
    
    			targetElement = forElement;
    		}
    	} else if (this.needsFocus(targetElement)) {
    		if ((event.timeStamp - trackingClickStart) > 100 || (this.deviceIsIOS && window.top !== window && targetTagName === 'input')) {
    			this.targetElement = null;
    			return false;
    		}
    
    		this.focus(targetElement);
    		if (!this.deviceIsIOS4 || targetTagName !== 'select') {
    			this.targetElement = null;
    			event.preventDefault();
    		}
    		return false;
    	}
    	if (this.deviceIsIOS && !this.deviceIsIOS4) {
    		scrollParent = targetElement.fastClickScrollParent;
    		if (scrollParent && scrollParent.fastClickLastScrollTop !== scrollParent.scrollTop) {
    			return true;
    		}
    	}
    	if (!this.needsClick(targetElement)) {
    		event.preventDefault();
    		this.sendClick(targetElement, event);
    	}
    	return false;
    };
    

    注意上面的代码中,event.preventDefault();会阻止真实的click事件的触发,因此,在button上面的click事件不会触发。

    接下来,我们只需要查看sendClick方法。

    FastClick.prototype.sendClick = function(targetElement, event) {
    	'use strict';
    	var clickEvent, touch;
    	if (document.activeElement && document.activeElement !== targetElement) {
    		document.activeElement.blur();
    	}
    	touch = event.changedTouches[0];
    
    	clickEvent = document.createEvent('MouseEvents');
           clickEvent.initMouseEvent('click', true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null); clickEvent.forwardedTouchEvent = true; targetElement.dispatchEvent(clickEvent); };

    在此方法中,会创建一个自定义的click事件,然后在button上立即触发,于是,button绑定的click的事件回调函数马上执行,因此就没有300ms延迟了。

    上面的initMouseEvent方法的前三个参数的意思:1.事件类型,2.是否冒泡,3.是否阻止浏览器的默认行为。

    自定义的click事件阻止了浏览器的默认行为,事件冒泡,于是执行document.body的click事件回调函数。代码如下:

    FastClick.prototype.onClick = function(event) {
    	'use strict';
    	var permitted;
    	if (this.trackingClick) {
    		this.targetElement = null;
    		this.trackingClick = false;
    		return true;
    	}
    	if (event.target.type === 'submit' && event.detail === 0) {
    		return true;
    	}
    	permitted = this.onMouse(event);
    	if (!permitted) {
    		this.targetElement = null;
    	}
    	return permitted;
    };
    

    然后里面有一句 permitted = this.onMouse(event);于是,我们查看onMouse方法:

    FastClick.prototype.onMouse = function(event) {
    	'use strict';
    	if (!this.targetElement) {
    		return true;
    	}
    	if (event.forwardedTouchEvent) {
    		return true;
    	}
    	if (!event.cancelable) {
    		return true;
    	}
    	if (!this.needsClick(this.targetElement) || this.cancelNextClick) {
    		if (event.stopImmediatePropagation) {
    			event.stopImmediatePropagation();
    		} else {
    			event.propagationStopped = true;
    		}
    		event.stopPropagation();
    		event.preventDefault();
    		return false;
    	}
    	return true;
    };
    

    此方法,会阻止模拟的click事件的冒泡以及默认行为。

    到此,解决了移动端浏览器click事件延迟300ms的问题。

      

      

      

    加油!  

  • 相关阅读:
    CodeForces
    hdu4003 树形dp
    hdu2196
    poj2486
    hdu1502 树形dp入门题
    cf 686D
    bzoj2763 分层图
    hdu4424 并查集+贪心+思维
    poj1734 最小环+输出路径
    集训题解1
  • 原文地址:https://www.cnblogs.com/chaojidan/p/4517895.html
Copyright © 2020-2023  润新知