TouchEnd事件处理中的那些坑我替你踩过了
TouchEnd事件处理的那些坑
昨天遇到一个奇怪的问题,移动端的点击效果怎么都触发不了,折腾了半天发现原来是touchend事件被preventDefault给阻止了。这里我把整个踩坑过程记录一下,免得以后再犯同样的错误。
事情是这样的,我在做一个移动端的滑动菜单组件,需要在touchstart的时候记录起始位置,touchmove的时候计算偏移量,touchend的时候判断是否达到阈值来决定是否切换菜单状态。按理说这应该是很基础的功能,但偏偏touchend就是不触发点击事件。
折腾半天才发现的真相
一开始我以为是事件冲突,试了各种event.stopPropagation()和stopImmediatePropagation(),都不管用。后来打印event对象发现了问题所在:touchend事件确实触发了,但是后面的click事件被阻断了。
查了一下资料才知道,在移动端,浏览器有个”fast click”机制,当touchend之后很快就会触发click事件。但如果在touchend里调用了preventDefault(),就会阻止这个click事件的触发。而我为了防止页面滚动,确实在touchmove里面加了preventDefault(),这就导致了连锁反应。
核心代码就这几行
我的原始代码大概是这样:
let startX = 0;
let startY = 0;
let isScrolling = false;
element.addEventListener('touchstart', function(e) {
const touch = e.touches[0];
startX = touch.clientX;
startY = touch.clientY;
isScrolling = undefined; // 重置滚动状态
});
element.addEventListener('touchmove', function(e) {
if (isScrolling === undefined) {
const touch = e.touches[0];
const deltaX = Math.abs(touch.clientX - startX);
const deltaY = Math.abs(touch.clientY - startY);
// 判断是垂直滚动还是水平滚动
isScrolling = deltaY > deltaX ? 'vertical' : 'horizontal';
}
// 关键问题在这里,如果判定为垂直滚动就要preventDefault
if (isScrolling === 'vertical') {
e.preventDefault(); // 这里阻止了默认行为
}
});
上面这段代码看起来没问题,但在实际使用中会导致touchend后的click事件失效。后来我修改了一下逻辑:
let startX = 0;
let startY = 0;
let isScrolling = false;
let shouldPreventClick = false;
element.addEventListener('touchstart', function(e) {
const touch = e.touches[0];
startX = touch.clientX;
startY = touch.clientY;
isScrolling = undefined;
shouldPreventClick = false; // 重置点击阻止标志
});
element.addEventListener('touchmove', function(e) {
if (isScrolling === undefined) {
const touch = e.touches[0];
const deltaX = Math.abs(touch.clientX - startX);
const deltaY = Math.abs(touch.clientY - startY);
isScrolling = deltaY > deltaX ? 'vertical' : 'horizontal';
}
if (isScrolling === 'vertical') {
e.preventDefault();
shouldPreventClick = true; // 标记需要阻止click
}
});
element.addEventListener('touchend', function(e) {
// 触发业务逻辑
handleTouchEnd();
// 在touchend事件中手动处理点击逻辑
setTimeout(() => {
if (!shouldPreventClick && !isScrolling) {
// 如果不需要阻止点击且没有滚动,则触发点击效果
element.click();
}
// 重置状态
isScrolling = false;
shouldPreventClick = false;
}, 0);
});
function handleTouchEnd() {
// 这里处理touchend的具体业务逻辑
console.log('touchend triggered');
}
踩坑提醒:这里注意我踩过好几次坑
上面的解决方案看似完美,但实际上还有个小问题。setTimeout里的逻辑可能会在某些情况下执行时机不对,特别是在快速连续操作的时候。后来我又优化了一下:
let startX = 0;
let startY = 0;
let isScrolling = false;
let touchStartTime = 0;
element.addEventListener('touchstart', function(e) {
const touch = e.touches[0];
startX = touch.clientX;
startY = touch.clientY;
isScrolling = undefined;
touchStartTime = Date.now();
});
element.addEventListener('touchmove', function(e) {
if (isScrolling === undefined) {
const touch = e.touches[0];
const deltaX = Math.abs(touch.clientX - startX);
const deltaY = Math.abs(touch.clientY - startY);
isScrolling = deltaY > deltaX ? 'vertical' : 'horizontal';
}
if (isScrolling === 'vertical') {
e.preventDefault();
}
});
element.addEventListener('touchend', function(e) {
const touchDuration = Date.now() - touchStartTime;
// 如果移动距离很小且持续时间短,认为是点击
const touch = e.changedTouches[0];
const endX = touch.clientX;
const endY = touch.clientY;
const moveDistance = Math.sqrt(
Math.pow(endX - startX, 2) +
Math.pow(endY - startY, 2)
);
// 距离小于10像素且时间小于200ms认为是点击
if (moveDistance < 10 && touchDuration < 200 && !isScrolling) {
// 延迟触发点击,给浏览器时间处理原生click
setTimeout(() => {
element.click();
}, 10);
} else {
// 处理滑动逻辑
handleSwipe();
}
// 重置状态
isScrolling = false;
});
还有个隐藏的bug
你以为这就完了?还没完呢!还有一个更隐蔽的问题:在某些Android机型上,touchend事件可能会延迟触发或者根本不触发。为了避免这种情况,我还加上了touchcancel事件的处理:
element.addEventListener('touchcancel', function(e) {
// 处理触摸取消的情况
isScrolling = false;
shouldPreventClick = false;
});
另外,考虑到性能问题,建议在不需要的时候及时移除这些事件监听器,特别是在SPA应用中页面切换的时候,避免内存泄漏。
最后的小贴士
经过这次折腾,我总结了一些经验:
- touchmove事件中的preventDefault()会影响后续的click事件,这是浏览器的行为而不是bug
- 判断滑动方向时不要立即preventDefault,而是先标记状态
- touchend事件的触发条件判断要综合考虑移动距离和时间
- 记得处理touchcancel事件,防止异常情况
虽然现在有了Pointer Events API可以统一处理各种输入设备,但在实际项目中我还是更倾向于用传统的touch系列事件,因为兼容性更好,而且控制更精确。
以上是我踩坑后的总结,希望对你有帮助。如果你有更好的方案欢迎评论区交流。

暂无评论