You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何实现touchstart/touchmove/touchend与鼠标事件同效处理?

解决触摸拖拽时兄弟元素无法激活的问题

我完全懂你遇到的困扰——鼠标拖拽能完美实现兄弟元素依次激活,但触摸操作就掉链子。这其实是触摸事件和鼠标事件的核心行为差异导致的,下面给你拆解问题根源和解决方案:

问题核心原因

触摸事件和鼠标事件的触发逻辑天生不同:

  • 鼠标事件:mousemove会实时触发在鼠标指针当前悬停的元素上,所以拖拽时能自然切换激活的兄弟元素。
  • 触摸事件:当touchstart触发后,浏览器会把后续所有touchmovetouchend事件锁定绑定到初始触发touchstart的元素,哪怕手指移到其他元素上,事件目标也不会改变。这就是触摸拖拽失效的关键。

解决方案:全局监听触摸事件 + 实时获取触摸位置元素

我们可以绕开触摸事件的目标锁定机制,通过在document级别监听touchmove,实时获取手指下方的元素,再手动控制激活状态。同时调整Vue组件的逻辑,让父组件统一管理激活状态,避免子组件各自为政。

修改后的完整代码

<!DOCTYPE html>
<html>
<head>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
  <style>
    * {
      -webkit-touch-callout: none;
      -webkit-user-select: none;
      -khtml-user-select: none;
      -moz-user-select: none;
      -ms-user-select: none;
      user-select: none;
    }
  </style>
</head>
<body>
  <div id="app" class="container py-4" @mouseup="exitPanMode" @touchend="exitPanMode">
    <div class="row">
      <to-press v-for="i of 12" :key="i" :id="`press-${i}`" :is-active="activeElements.includes(`press-${i}`)" @touchstart="enterPanMode" @mousedown="enterPanMode" />
    </div>
    <p>Click/touch a rectangle, hold and drag across</p>
  </div>

  <script>
    Vue.component('to-press', {
      template: `<span class="col-3 p-2 bg-light border" :class="{ 'bg-dark': isActive }">&nbsp;</span>`,
      props: ['isActive', 'id']
    })

    const app = new Vue({
      el: "#app",
      data() {
        return {
          panMode: false,
          activeElements: []
        };
      },
      methods: {
        enterPanMode(e) {
          this.panMode = true;
          // 激活初始触摸/点击的元素
          const elementId = e.target.id;
          if (!this.activeElements.includes(elementId)) {
            this.activeElements.push(elementId);
          }
          // 绑定全局移动事件监听
          if (e.type === 'touchstart') {
            document.addEventListener('touchmove', this.handleTouchMove);
          } else {
            document.addEventListener('mousemove', this.handleMouseMove);
          }
        },
        exitPanMode() {
          this.panMode = false;
          this.activeElements = [];
          // 移除全局监听,避免内存泄漏
          document.removeEventListener('touchmove', this.handleTouchMove);
          document.removeEventListener('mousemove', this.handleMouseMove);
        },
        handleTouchMove(e) {
          e.preventDefault(); // 阻止默认触摸滚动,按需选择是否保留
          // 获取当前触摸位置的元素
          const touch = e.touches[0];
          const targetElement = document.elementFromPoint(touch.clientX, touch.clientY);
          if (targetElement && targetElement.id?.startsWith('press-')) {
            const elementId = targetElement.id;
            if (!this.activeElements.includes(elementId)) {
              this.activeElements.push(elementId);
            }
          }
        },
        handleMouseMove(e) {
          const targetElement = e.target;
          if (targetElement && targetElement.id?.startsWith('press-')) {
            const elementId = targetElement.id;
            if (!this.activeElements.includes(elementId)) {
              this.activeElements.push(elementId);
            }
          }
        }
      }
    })
  </script>
</body>
</html>

关键修改点说明

  1. 统一激活状态管理:把原来每个子组件的isActive状态迁移到父组件的activeElements数组,由父组件统一控制哪些元素需要激活。
  2. 全局事件监听
    • touchstart/mousedown时绑定全局的touchmove/mousemove监听,确保能跟踪到整个页面内的移动行为。
    • touchmove中用document.elementFromPoint()获取手指当前位置的元素,这是绕过触摸目标锁定的核心技巧。
  3. 清理事件监听:在touchend/mouseup时移除全局监听,避免不必要的性能消耗和内存泄漏。
  4. 可选的默认行为阻止e.preventDefault()可以避免触摸时的页面滚动干扰,如果你需要保留页面滚动功能,可以去掉这行代码。

额外优化建议

如果你的需求是只激活当前手指下的元素(而不是所有经过的元素),可以把activeElements改成单个字符串变量,每次替换成当前元素的ID即可,逻辑会更简洁。

内容的提问来源于stack exchange,提问作者Pierre Burton

火山引擎 最新活动