通过fastclick源码分析彻底解决tap“点透”
背景
在移动端开发过程中,常常会遇到“点透”的问题。例如,当一个元素的click事件和另一个元素的touchend事件同时被触发时,就会发生“点透”,相当于用户点了下下一层的元素。为了避免这种问题的出现,我们可以使用第三方库 fastclick 来解决这一问题,此处将通过 fastclick 的源码分析来解决这个问题。
fastclick 原理
fastclick 的核心是通过对 click 事件在移动设备上的延迟处理机制进行封装,启用 touch 事件模拟 click 事件并在合适的时候阻止默认行为从而彻底解决移动端的点透问题。在 fastclick 的实现中,主要依赖了 touchstart、touchend 和 click 事件,具体实现可以参考下面的代码:
var FastClick = function(layer, options) {
var oldOnClick;
options = options || {};
// If necessary, set options to the global default settings
for (var key in FastClick.defaults) {
if (typeof options[key] === 'undefined') {
options[key] = FastClick.defaults[key];
}
}
// ......
function onTouchStart(event) {
if (event.targetTouches.length > 1) {
return true;
}
if (options.disableInput) {
event.preventDefault();
}
downEl = hasTouch ? event.target : (event.srcElement || element);
addClass(downEl, options.activeClass);
lastTouchStartTime = Date.now();
clearTimeout(activeTimeout);
return true;
}
// ......
function onClick(event) {
if (deviceIsIOS && !event.metaKey && !event.ctrlKey) {
event.preventDefault();
}
}
// ......
function onMouseUp(event) {
if (trackingClick) {
targetElement = event.target || event.srcElement;
targetTagName = targetElement.tagName.toLowerCase();
removeClass(downEl, options.activeClass);
trackingClick = false;
if (targetTagName === 'label') {
forElement = findControl(targetElement);
if (forElement) {
focus(targetElement);
if (deviceIsAndroid) {
return false;
}
targetElement = forElement;
}
} else if (shouldUseClick(targetElement)) {
doClick(targetElement, event);
event.preventDefault();
}
return false;
}
return true;
}
// ......
function onDoubleClick(event) {
// Certain devices have specific user agent strings that require even more
// hacky solutions to detect whether they are mobile. These are provided
// in the big if clause which follows.
if (deviceIsAndroid && (Date.now() - lastTouchEndTime) < 200) {
// Trick the browser into believing that the user had actually
// tapped on the target element by manually setting up touch coordinates
// for it
resetOnTouchEnd();
targetElement = event.target || event.srcElement;
oldTargetElement = targetElement;
simulateEvent(targetElement, 'mousedown');
// 延迟触发移除 class and preventDefault
addClass(targetElement, options.activeClass);
activeTimeout = setTimeout(function() {
removeClass(targetElement, options.activeClass);
}, options.activeTimeout);
simulateEvent(targetElement, 'mouseup');
simulateEvent(targetElement, 'click');
}
}
// 为某个元素绑定事件
function fastClick() {
var layer = this;
// 为特定 layer 添加 fastclick 特性
addEvent(layer, 'touchstart', onTouchStart);
addEvent(layer, 'touchmove', onTouchMove);
addEvent(layer, 'touchend', onTouchEnd);
addEvent(layer, 'touchcancel', onTouchCancel);
addEvent(layer, 'mousedown', onMouseDown);
addEvent(layer, 'mouseup', onMouseUp);
addEvent(layer, 'mousemove', onMouseMove);
addEvent(layer, 'click', onClick);
addEvent(layer, 'dblclick', onDoubleClick);
// ....
}
// Attach FastClick to object
// 将 fastclick 挂载在某一个元素上
FastClick.attach = function(layer, options) {
return new FastClick(layer, options);
};
// ....
// 绑定 fastclick
if (typeof window.ontouchstart === 'undefined' && !deviceIsAndroid) {
document.addEventListener('DOMContentLoaded', function() {
FastClick.attach(document.body);
}, false);
}
// ....
};
fastclick 实现
fastclick 本身比较简单,通过给一个对象的 layer 属性绑定事件,然后在事件处理的过程中阻止默认事件从而达到消除点透的目的。我们常常使用:
if ('addEventListener' in document) {
document.addEventListener('DOMContentLoaded', function() {
FastClick.attach(document.body);
}, false);
}
将 fastclick 应用到页面上来消除点透。但是,这并不能解决所有的问题。我们还需要解决:为什么在移动端环境下快速触发2次click事件时,会出现1次“点透”的问题。fastclick 的原理的概括如下:
- 处理 touchstart 事件,如果目标元素是表单控件元素,就不处理,默认使用系统点击
- 处理 touchmove 事件,记录当前不发布 click 事件,防止滑动时有误点击
- 处理 touchend 事件,触发 click 事件,如果时间大于 300ms,认为不是快速点击,不处理不阻止
- 处理 click 事件,阻止事件默认行为
以下是 fastclick 的使用示例:
<body>
<div class="button">Click me!</div>
<script src="//cdn.bootcss.com/fastclick/1.0.6/fastclick.min.js"></script>
<script>
window.addEventListener('load', function() {
FastClick.attach(document.body);
}, false);
</script>
</body>
解决方案
针对“为什么在移动端环境下快速触发2次click事件时,会出现1次“点透”的问题”,我们需要加入一个遮罩层来解决。点击一次时,在 300ms 内尝试拦截所有的后续点击事件,此时使用一个类为 in-acting 的遮罩层将当前点击目标元素覆盖,拦截所有的 click 事件,然后在 300ms 后移除遮罩层,释放 click 事件到目标元素。修改的具体实现可以参考下面的代码:
// 自定义 fastclick
var FC = function(layer){
// ..
var self = this;
self.threshold = options.threshold || 10; // px
self.layer = layer;
self.reset = function(){
self.trackingClick = false;
self.trackingClickStart = 0;
self.targetElement = null;
self.lastTouchIdentifier = self.touchIdentifier;
self.touchStartX = 0;
self.touchStartY = 0;
self.lastTouchMove = 0;
self.touchBoundary = options.touchBoundary || 10;
self.layer.removeEventListener('touchend', self, false);
self.layer.removeEventListener('touchcancel', self, false);
self.layer.removeEventListener('touchmove', self, false);
document.removeEventListener('scroll', self, true);
}
// ..
self.layer.addEventListener('touchstart', self, false);
self.layer.addEventListener('touchmove', self, false);
self.layer.addEventListener('touchend', self, false);
self.layer.addEventListener('touchcancel', self, false);
};
FC.prototype.handleEvent = function(event){
switch(event.type){
case 'touchstart': return this.onTouchStart(event);
case 'touchmove': return this.onTouchMove(event);
case 'touchend': return this.onTouchEnd(event);
case 'touchcancel': return this.onTouchCancel(event);
}
}
FC.prototype.onTouchStart = function(event){
console.log(event);
if(this.trackingClick) return;
this.trackingClick = true;
this.targetElement = event.target;
this.touchIdentifier = event.touches[0].identifier;
this.trackingClickStart = Date.now();
var self = this;
this.layer.addEventListener('touchmove', self, false);
this.layer.addEventListener('touchend', self, false);
this.layer.addEventListener('touchcancel', self, false);
}
FC.prototype.onTouchEnd = function(event){
console.log(event);
if(!this.trackingClick) return;
if((Date.now() - this.trackingClickStart) > 300) return this.reset();
this.preventDefault(event);
this.stopEventPropagation(event);
this.reset();
return false;
}
FC.prototype.onTouchMove = function(event){
// console.log(event);
if(!this.trackingClick) return true;
if(this.lastTouchIdentifier !== event.touches[0].identifier) return this.reset();
var dx = event.touches[0].pageX - this.touchStartX;
var dy = event.touches[0].pageY - this.touchStartY;
if((dx * dx + dy * dy) > (this.threshold * this.threshold)) return this.reset();
}
FC.prototype.onTouchCancel = function(event){
console.log(event);
this.preventDefault(event);
this.stopEventPropagation(event);
this.reset();
return false;
}
FC.prototype.preventDefault = function(event){
event.preventDefault();
}
FC.prototype.stopEventPropagation = function(event){
event.stopPropagation();
}
var addCustomFastClick = function(){
var inAction = false,
isTouch = false;
$('body').on('touchend', function(e){
if(!isTouch){
return;
}
inAction = true;
$(e.target).addClass('in-acting');
setTimeout(function(){
$('body .in-acting').removeClass('in-acting');
inAction = false;
}, 300);
}).on('touchstart', function(e){
isTouch = true;
}).on('touchmove', function(e){
isTouch = false;
});
return;
};
addCustomFastClick();
我们自定义了一个 fastclick,实质类似于解决方案1,即阻止后续 300ms 内的事件(除了第一个事件)并添加遮罩层,此处的遮罩层使用的类是 in-acting。
解决方案1示例
<body>
<div class="button">Click me!</div>
<script src="//cdn.bootcss.com/fastclick/1.0.6/fastclick.min.js"></script>
<script>
if ('addEventListener' in document) {
document.addEventListener('DOMContentLoaded', function() {
FastClick.attach(document.body);
}, false);
}
// 自定义的处理遮罩层
function addCustomFastClick() {
var inAction = false,
isTouch = false;
$('body').on('touchend', function(e) {
if (!isTouch) {
return;
}
inAction = true;
$(e.target).addClass('in-acting');
setTimeout(function() {
$('body .in-acting').removeClass('in-acting');
inAction = false;
}, 300);
}).on('touchstart', function(e) {
isTouch = true;
}).on('touchmove', function(e) {
isTouch = false;
});
return;
}
addCustomFastClick();
</script>
</body>
解决方案2示例
<body>
<div class="button">Click me!</div>
<script>
var fc = new FC(document.querySelector('.button'));
</script>
</body>
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:通过fastclick源码分析彻底解决tap“点透” - Python技术站