TouchMove事件在移动端开发中的常见问题与解决方案
又踩坑了,touchmove滚动失效
最近在做一个移动端的手势滑动功能,本来以为touchmove这种老生常谈的东西没什么好纠结的,结果一动手才发现,不同的处理方案差别还挺大。主要是页面里有滚动区域,有时候手势滑动和页面滚动冲突,有时候又需要精确控制滑动距离。
我用了几种主流方案对比了一下,今天就来聊聊各自的优缺点。
三种主流touchmove处理方案
一般来说,touchmove的处理主要有三种方式:
- 原生JavaScript直接绑定事件
- 使用防抖/节流优化的方案
- 集成手势库(比如hammer.js)
每种都有适用场景,我先说说我最后的选择:大部分情况下我比较喜欢用原生+手动优化的方式,灵活性最高,而且体积也够小。
谁更灵活?谁更省事?
先看原生方案,核心代码就这么几行:
let startY = 0;
let currentY = 0;
element.addEventListener('touchstart', (e) => {
startY = e.touches[0].clientY;
});
element.addEventListener('touchmove', (e) => {
e.preventDefault(); // 阻止默认滚动行为
currentY = e.touches[0].clientY;
const moveDistance = currentY - startY;
// 处理滑动逻辑
handleSwipe(moveDistance);
});
element.addEventListener('touchend', (e) => {
// 手势结束逻辑
resetSwipe();
});
这个方案的优势很明显:完全可控,想怎么处理就怎么处理,性能也不错。但是有个坑就是event.preventDefault()的时机很重要,用不好会导致页面滚动失效或者手势响应卡顿。
再看看加了防抖的版本:
let startY = 0;
let lastTime = 0;
element.addEventListener('touchstart', (e) => {
startY = e.touches[0].clientY;
});
element.addEventListener('touchmove', (e) => {
const now = Date.now();
if (now - lastTime < 16) return; // 限制频率
lastTime = now;
const currentY = e.touches[0].clientY;
const moveDistance = currentY - startY;
requestAnimationFrame(() => {
handleSwipe(moveDistance);
});
});
这种方案解决了高频触发的问题,滑动会更平滑。但调试起来比较麻烦,时间间隔的阈值需要反复测试才能找到最佳数值。
至于手势库,Hammer.js确实方便,几行代码就能搞定复杂手势:
const mc = new Hammer(element);
mc.get('swipe').set({ direction: Hammer.DIRECTION_VERTICAL });
mc.on('swipeup swipedown', (ev) => {
handleSwipe(ev.direction);
});
但问题是增加了包体积,而且某些特殊需求用现成的API可能实现不了,还是要回到原生方案。
踩坑提醒:这三点一定注意
首先说一下touchmove中最容易踩的坑——页面滚动冲突。这个问题我折腾了好几个小时才发现,原来是event.preventDefault()的位置不对。如果在touchmove回调函数开头就调用preventDefault,可能会阻止整个页面的滚动,用户体验很糟糕。
后来我发现一个更好的做法是在确定需要拦截滚动的时候才阻止:
element.addEventListener('touchmove', (e) => {
// 先判断是否需要阻止默认行为
if (shouldPreventDefault()) {
e.preventDefault();
}
// 继续处理业务逻辑
const currentY = e.touches[0].clientY;
handleSwipe(currentY - startY);
});
第二个坑是多指触控的处理。刚开始测试的时候只考虑了单指的情况,结果上线后用户多指操作导致坐标计算异常。解决办法是在touchstart和touchmove里都判断一下touches的数量:
element.addEventListener('touchstart', (e) => {
if (e.touches.length !== 1) return;
startY = e.touches[0].clientY;
});
element.addEventListener('touchmove', (e) => {
if (e.touches.length !== 1) return;
// 处理滑动...
});
第三个坑其实不算bug,但在真实项目中很常见:某些安卓机型对touch事件的支持不够好,会有延迟或者不触发的情况。这个没办法从根本上解决,只能通过一些兼容性处理来缓解,比如同时监听mouse事件。
我的选型逻辑
基于这些实践经验,我现在有了比较固定的选型逻辑:
简单的上下滑动、轮播图这种场景,我会用原生方案,自己封装一个通用的touch handler。因为这种场景下对性能和灵活性要求比较高,而且手势逻辑相对简单,没必要引入额外依赖。
复杂的多手势交互,比如缩放、旋转、多方向滑动同时存在,那肯定选择手势库,省事又稳定。不过Hammer.js现在更新不太频繁了,新项目我会考虑AlloyFinger这种轻量级替代。
性能敏感的场景,比如游戏类应用或者动画较多的功能,我会用原生+requestAnimationFrame的组合,确保每一帧都能及时处理。
还有一个经验就是,不管用哪种方案,都要考虑到iOS和Android的差异。特别是iOS的bounce效果,有时候会让你的touchmove处理出现意外情况。
最后提一个细节优化,touchmove事件的被动监听器设置很重要:
// 这样设置可以提升滚动性能
element.addEventListener('touchmove', handler, { passive: false });
passive设为false才能使用preventDefault,但会影响性能。如果不需要阻止默认行为,就设为true。
以上是我个人对这几种touchmove处理方案的完整对比,主要还是看具体需求。有更优的实现方式欢迎评论区交流。

暂无评论