移动端快速滑动后点击触发多次事件怎么解决?

Tr° 熙研 阅读 59

在开发移动端列表时遇到了奇怪的问题,当用户快速滑动列表后松手,偶尔会触发多余的点击事件。我用touchstarttouchend计算坐标差来模拟点击,但滑动结束时如果手指短暂悬停就会误触。

试过给点击事件加防抖:setTimeout延迟执行,但还是会出现两次点击回调。查看代码发现,当垂直滑动距离小于10px时会被判定为点击,这会不会和原生点击有冲突?

代码片段类似这样:


let startX, startY;
element.addEventListener('touchstart', (e) => {
  startX = e.touches[0].clientX;
  startY = e.touches[0].clientY;
});
element.addEventListener('touchend', (e) => {
  const endX = e.changedTouches[0].clientX;
  const endY = e.changedTouches[0].clientY;
  if (Math.abs(endX - startX) < 10 && Math.abs(endY - startY) < 10) {
    handleClick();
  }
});

有没有更好的手势判断方式?或者需要同时监听touchmove来判断滑动方向?

我来解答 赞 5 收藏
二维码
手机扫码查看
2 条解答
W″颖杰
这个问题确实挺常见的,特别是移动端手势处理这块。WP里面虽然不会直接用到这样的原生JS代码,但原理是一样的。你的思路没问题,不过确实需要改进一下逻辑。

首先,touchmove 是必须要监听的,这样才能更准确判断用户到底是在滑动还是点击。你可以加一个标志位来标记是否在滑动,这样就避免了滑动结束时误触的问题。

另外,防抖(setTimeout)和延迟执行也可以保留,进一步确保事件不会被重复触发。下面是改进后的代码:

let startX, startY, isScrolling;

element.addEventListener('touchstart', (e) => {
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
isScrolling = false; // 初始化滚动状态
});

element.addEventListener('touchmove', () => {
isScrolling = true; // 只要检测到移动,就标记为滚动
});

element.addEventListener('touchend', (e) => {
if (isScrolling) return; // 如果标记为滚动,直接退出

const endX = e.changedTouches[0].clientX;
const endY = e.changedTouches[0].clientY;

if (Math.abs(endX - startX) < 10 && Math.abs(endY - startY) < 10) {
handleClick(); // 这里还可以加个防抖
}
});


这种写法的好处是,把滑动和点击彻底分开了。isScrolling 这个标志位很关键,它能帮你精准判断用户的行为。

顺便说一句,如果你用的是jQuery或者一些UI框架,可能已经有封装好的手势库了,直接用会省事很多。不过既然你这里用的是原生JS,上面这个方案应该能完美解决你的问题。
点赞 10
2026-02-01 17:03
维通~
维通~ Lv1
这个问题的关键在于,移动端的触摸事件本身有比较复杂的交互逻辑,特别是在滑动和点击之间的边界判定上。你的代码确实会因为滑动结束时手指短暂悬停而误判为点击事件,这是很常见的问题。

### 解决思路

1. **引入防抖机制**:虽然你已经尝试了setTimeout,但可能方式还不够精准。我们需要确保在一次手势操作中,无论是滑动还是点击,都只能触发一个回调。
2. **监听touchmove事件**:通过检测是否有滑动行为发生,可以更准确地判断当前手势是滑动还是点击。
3. **使用标志位控制事件流**:用一个标志位来区分滑动和点击,避免重复触发。

下面是改进后的代码实现:

// 定义标志位
let isScrolling = false;

// touchstart 事件
element.addEventListener('touchstart', (e) => {
// 记录起始坐标
const startX = e.touches[0].clientX;
const startY = e.touches[0].clientY;

// 初始化标志位
isScrolling = false;

// 保存起始坐标到自定义属性中(方便后续访问)
e.target.dataset.startX = startX;
e.target.dataset.startY = startY;
});

// touchmove 事件
element.addEventListener('touchmove', (e) => {
// 如果已经有滑动行为,则设置标志位
const startX = parseFloat(e.target.dataset.startX);
const startY = parseFloat(e.target.dataset.startY);
const currentX = e.touches[0].clientX;
const currentY = e.touches[0].clientY;

const deltaX = Math.abs(currentX - startX);
const deltaY = Math.abs(currentY - startY);

// 如果滑动距离超过阈值,判定为滑动
if (deltaX > 10 || deltaY > 10) {
isScrolling = true;
}
});

// touchend 事件
element.addEventListener('touchend', (e) => {
// 如果没有滑动行为,并且移动距离小于阈值,则判定为点击
if (!isScrolling) {
const endX = e.changedTouches[0].clientX;
const endY = e.changedTouches[0].clientY;

const startX = parseFloat(e.target.dataset.startX);
const startY = parseFloat(e.target.dataset.startY);

const deltaX = Math.abs(endX - startX);
const deltaY = Math.abs(endY - startY);

if (deltaX < 10 && deltaY < 10) {
handleClick();
}
}

// 清空标志位和自定义属性
isScrolling = false;
e.target.dataset.startX = null;
e.target.dataset.startY = null;
});


### 为什么这样改?

1. **标志位的作用**:isScrolling用来标记用户是否进行了滑动操作。如果在touchmove中检测到滑动距离超过阈值,就将该标志位置为true,从而阻止后续的点击事件触发。
2. **滑动和点击的分离**:通过在touchmove中实时计算滑动距离,我们可以在滑动发生时立即识别并屏蔽点击行为。
3. **数据存储优化**:为了避免全局变量污染,我们将起始坐标存储在事件目标的dataset属性中,这是一种比较优雅的方式来管理临时数据。
4. **清理工作**:每次手势结束后,都要清空标志位和存储的数据,防止影响下一次手势操作。

### 其他注意事项

- 如果你的列表项中有嵌套链接或者其他可点击元素,可能会导致事件冒泡或捕获问题。可以考虑在handleClick中调用e.stopPropagation()来阻止事件传播。
- 阈值10px可以根据实际需求调整,但不要设得太小,否则容易误判。

这种方式基本上能解决滑动后点击事件被误触的问题,同时也兼顾了性能和用户体验。开发者的生活就是不断和这些细节斗智斗勇啊,希望这个方案能帮到你!
点赞 3
2026-01-31 22:03