TouchMove事件在移动端开发中的常见问题与解决方案

极客瑞君 交互 阅读 1,489
赞 31 收藏
二维码
手机扫码查看
反馈

又踩坑了,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处理方案的完整对比,主要还是看具体需求。有更优的实现方式欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论