跳到主要内容

移动端事件之 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被确定,那么在后续的touchmovetouchend事件中,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>

在上面的代码中,我们封装了taplongTap两个方法,分别用于处理单击和长按事件。通过判断touchstarttouchend事件的时间间隔,可以确定是单击还是长按。

穿透问题

由于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>