移动端事件之 touch
click 事件 300ms 延迟的问题
在 iPhone 第一代推出时,为了将 PC 端的大页面适配到手机的小屏幕上,iPhone 提供了页面缩放和双击放大的功能。这导致了每次单击都需要等待 300ms,以判断用户是否会进行第二次点击。因此,移动端的dblclick
事件从此就失效了。
为了解决上述问题,移动端新增了touch
事件。
touch 事件
touchstart
事件在手指按下时触发。
touchmove
事件在手指在屏幕上滑动时触发。
touchend
事件在手指抬起时触发。
touchcancel
事件在滑动屏幕时,如果来电话等情况阻止了继续滑动,就会触发该事件。
document.addEventListener(
'touchstart',
function () {
console.log('touchstart');
},
false
);
document.addEventListener(
'touchmove',
function () {
console.log('touchmove');
},
false
);
document.addEventListener(
'touchend',
function () {
console.log('touchend');
},
false
);
document.addEventListener(
'touchcancel',
function () {
console.log('touchcancel');
},
false
);
setTimeout(() => {
alert('blocked');
}, 3000);
e.target
一旦touchstart
事件的e.target
被确定,那么在后续的touchmove
和touchend
事件中,e.target
就一直是touchstart
事件的目标元素。
document.addEventListener(
'touchstart',
function (e) {
console.log(e.target);
},
false
);
document.addEventListener(
'touchmove',
function (e) {
console.log(e.target);
},
false
);
document.addEventListener(
'touchend',
function (e) {
console.log(e.target);
},
false
);
e.touches
e.touches
属性中保存了一个TouchList
对象,其中列出了所有当前与触摸表面接触的Touch
对象,不管触摸点是否已经改变或其目标元素是否处于touchstart
阶段。
它保存了所有的触点信息,可以通过identifier
属性找到每个触点的唯一标识。
e.changedTouches
e.changedTouches
属性中保存了一个TouchList
对象,其中包含了与当前事件相关的所有触点。
e.targetTouches
e.targetTouches
属性中也保存了一个TouchList
对象,其中包含了作用在当前元素上的所有触点。
封装 touch 事件
我们可以封装touch
事件来实现单击和长按功能:
<body>
<div id="box" style="width: 100px; height: 100px; background-color: orange;"></div>
<script>
(function (doc) {
var Touch = function (selector) {
return Touch.prototype.init(selector);
};
Touch.prototype = {
init: function (selector) {
if (typeof selector == 'string') {
this.elem = doc.querySelector(selector);
return this;
}
},
tap: function (callback) {
this.elem.addEventListener('touchstart', handleTap, false);
this.elem.addEventListener('touchend', handleTap, false);
var startTime, endTime;
function handleTap(e) {
e.preventDefault();
switch (e.type) {
case 'touchstart':
startTime = Date.now();
break;
case 'touchend':
endTime = Date.now();
if (endTime - startTime < 500) {
callback.call(this, e);
}
break;
}
}
},
longTap: function (callback) {
this.elem.addEventListener('touchstart', handleLongTap, false);
this.elem.addEventListener('touchmove', handleLongTap, false);
this.elem.addEventListener('touchend', handleLongTap, false);
var timer = null;
var self = this;
function handleLongTap(e) {
switch (e.type) {
case 'touchstart':
timer = setTimeout(function () {
callback.call(self, e);
}, 500);
break;
case 'touchmove':
clearTimeout(timer);
timer = null;
break;
case 'touchend':
clearTimeout(timer);
timer = null;
break;
}
}
},
};
window.$ = window.Touch = Touch;
})(document);
$('#box').tap(function (e) {
console.log('tap');
});
$('#box').longTap(function (e) {
console.log('long tap');
});
</script>
</body>
在上面的代码中,我们封装了tap
和longTap
两个方法,分别用于处理单击和长按事件。通过判断touchstart
和touchend
事件的时间间隔,可以确定是单击还是长按。
穿透问题
由于click
事件存在 300ms 延迟的问题,会引起touch
事件的穿透。出现这种情况时,可以使用上面封装的touch
事件来解决。
下面是一个穿透问题的示例
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<style>
.mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
}
button,
input {
width: 100px;
height: 60px;
border: 1px solid #ccc;
}
</style>
<body>
<div class="mask"></div>
<button id="btn">btn</button><br />
<a href="https://www.baidu.com/" id="link">link</a><br />
<input type="text" />
<script>
var btn = document.getElementById('btn');
var mask = document.querySelector('.mask');
mask.addEventListener('touchstart', function () {
this.style.display = 'none';
});
btn.onclick = function () {
console.log('button clicked');
};
</script>
</body>
</html>
解决穿透的方法
延迟隐藏
mask.addEventListener('touchstart', function () {
var self = this;
setTimeout(function () {
self.style.display = 'none';
}, 300);
});
通过延迟 300ms 隐藏遮罩层,可以避免click
事件被触发。
中间层
在用户点击时立即触发touchstart
事件,由于事件落在了中间层上,所以不会触发click
事件。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<style>
.mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
z-index: 2;
}
.opacity-mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
button,
input {
width: 100px;
height: 60px;
border: 1px solid #ccc;
}
</style>
<body>
<div class="mask"></div>
<div class="opacity-mask"></div>
<button id="btn">btn</button><br />
<a href="https://www.baidu.com/" id="link">link</a><br />
<input type="text" />
<script>
var btn = document.getElementById('btn');
var mask = document.querySelector('.mask');
var opacityMask = document.querySelector('.opacity-mask');
mask.addEventListener('touchstart', function () {
var self = this;
this.style.display = 'none';
setTimeout(function () {
opacityMask.style.display = 'none';
}, 300);
});
btn.onclick = function () {
console.log('button clicked');
};
</script>
</body>
</html>
fastclick.js
fastclick是一个专门用于解决移动端click
事件 300ms 延迟问题的库。使用方法如下
<!-- 引入fastclick.js -->
<script src="./fastclick.js"></script>
<script>
// 使用FastClick
FastClick.attach(document.body);
var btn = document.getElementById('btn');
var mask = document.querySelector('.mask');
// 将touchstart改为click
mask.addEventListener('click', function () {
this.style.display = 'none';
});
btn.onclick = function () {
console.log('button clicked');
};
</script>