VueUse useSwipe 全面指南:基于 TouchEvent 的响应式滑动方向检测

原创2026-10-02 22:54:211,729 阅读
文章标签:前端

VueUse useSwipe 全面指南:基于 TouchEvent 的响应式滑动方向检测

useSwipe 是 VueUse 核心包(@vueuse/core)中一个轻量的 Sensors 类组合式函数,它基于原生 TouchEvent 为任意 DOM 元素提供响应式的滑动检测能力——包括滑动是否进行中(isSwiping)、滑动方向(direction)、滑动的横纵位移(lengthX / lengthY)等。在本文中,你将掌握 useSwipe 的完整 API、四个方向的判定原理、被动监听与 preventDefault 的配合方式,并基于仓库内的源码与测试用例理解其内部实现,最终能在移动端 Web 项目中实现滑动删除、轮播卡片等交互。

useSwipe 是什么

useSwipe 的核心职责是:把一段手势滑动"翻译"成一组响应式状态。你只需要传入一个元素引用(或 ref),它就会自动在目标元素上挂载 touchstart、touchmove、touchend / touchcancel 三个事件监听,并输出:

  • isSwiping:是否正处于滑动过程中;
  • direction:滑动方向(up / down / left / right / none);
  • coordsStart、coordsEnd:起止触点坐标;
  • lengthX、lengthY:累计的横向 / 纵向位移;
  • stop:手动解除所有监听。

它被统一导出在 core 包入口(export * from './useSwipe'),因此只需 import { useSwipe } from '@vueuse/core' 即可使用。

快速上手

官方文档 useSwipe 说明 给出的最小用法如下:

<script setup lang="ts">
import { useSwipe } from '@vueuse/core'
import { useTemplateRef } from 'vue'

const el = useTemplateRef('el')
const { isSwiping, direction } = useSwipe(el)
</script>

<template>
  <div ref="el">
    Swipe here
  </div>
</template>

注意两点:

  1. target 参数的类型是 MaybeRefOrGetter<EventTarget | null | undefined>,即可以传入模板引用、ref、getter 函数,甚至直接传入元素本身;配合 Vue 3.5+ 的 useTemplateRef 使用最为自然。
  2. isSwiping、direction 都是响应式值,在模板或 watch 中可以直接使用。

完整 API:配置参数(UseSwipeOptions)

源码中的 UseSwipeOptions 定义在 useSwipe/index.ts,它继承了 ConfigurableWindow(即额外的 window?: Window 选项,用于 iframe 或测试环境指定自定义 window 实例,见 _configurable.ts)。其余参数如下:

参数 类型 默认值 说明
passive boolean true 是否以 passive 模式注册事件监听。设置为 false 时,监听会以 capture 模式注册,并允许在 touchmove 中调用 preventDefault() 阻止页面滚动
threshold number 50 判定"有效滑动"的位移阈值(单位 px)。横纵位移中任一绝对值达到该值后,isSwiping 才会变为 true,direction 才会脱离 none
onSwipeStart (e: TouchEvent) => void 无 手指按下(touchstart)时触发的回调
onSwipe (e: TouchEvent) => void 无 滑动过程中(touchmove)触发的回调,仅在已判定为滑动中时触发
onSwipeEnd (e: TouchEvent, direction: UseSwipeDirection) => void 无 滑动结束(touchend / touchcancel)时触发的回调,第二个参数携带最终的方向

一个带完整回调的调用示例:

const { isSwiping, direction, lengthX, lengthY } = useSwipe(el, {
  threshold: 60,
  passive: false,
  onSwipeStart(e) {
    console.log('开始滑动', e.touches[0].clientX, e.touches[0].clientY)
  },
  onSwipe(e) {
    console.log('滑动中,横向位移', lengthX.value)
  },
  onSwipeEnd(e, dir) {
    console.log('结束,方向', dir)
  },
})

返回值详解(UseSwipeReturn)

useSwipe 的返回值类型定义在 useSwipe/index.ts,共七个成员:

返回值 类型 说明
isSwiping ShallowRef<boolean> 是否处于滑动中,越过 threshold 后变为 true
direction ComputedRef<UseSwipeDirection> 计算出的滑动方向:`'up'
coordsStart Readonly<Position> 起始触点坐标 { x, y }(Position 类型定义在 types.ts)
coordsEnd Readonly<Position> 最近一次 touchmove 的触点坐标 { x, y }
lengthX ComputedRef<number> 横向位移(coordsStart.x - coordsEnd.x),手指向右滑时为负值
lengthY ComputedRef<number> 纵向位移(coordsStart.y - coordsEnd.y),手指向下滑时为负值
stop () => void 手动解除全部三个事件监听

值得留意的是 lengthX / lengthY 的符号约定:它们由 coordsStart - coordsEnd 计算得出(见 index.ts)。手指向右滑动时 coordsEnd.x 增大,lengthX 为负;这一约定与官方演示中"右滑使 lengthX 为负、据此做位移"的逻辑完全一致。

方向判定原理:从源码看四个方向如何算出

方向不是简单地"最后一点在哪边",而是有一套严格的判定流程,见 index.ts:

const direction = computed((): UseSwipeDirection => {
  if (!isThresholdExceeded.value)
    return 'none'

  if (abs(diffX.value) > abs(diffY.value)) {
    return diffX.value > 0
      ? 'left'
      : 'right'
  }
  else {
    return diffY.value > 0
      ? 'up'
      : 'down'
  }
})

判定规则可以归纳为三步:

  1. 阈值门禁:max(|diffX|, |diffY|) >= threshold 才继续,否则一律返回 'none'(这一判断对应 isThresholdExceeded 计算属性,见 index.ts)。这避免了手指轻微抖动被误判为滑动。
  2. 主轴选择:比较 |diffX| 与 |diffY|,位移更大的方向为主轴,即使手指轨迹是斜线也只会得到一个主轴方向。
  3. 符号定方向:在主轴上,位移为正是因为起点坐标大于终点坐标(手指向左 / 向上),分别得到 left / up;反之得到 right / down。

这套逻辑保证了方向判定的确定性与稳定性,测试用例 index.test.ts 用 it.each 覆盖了 up、down、left、right 四种场景,并断言 onSwipeEnd 回调收到的方向参数与 direction 计算结果一致。

内部工作流:touchstart → touchmove → touchend

useSwipe 的实现非常精简(整个函数约 90 行),完全建立在 useEventListener 之上。useEventListener 通过 watchImmediate 在目标可用时注册 addEventListener,并在组件卸载或目标变化时自动清理(onCleanup 中逐个 removeEventListener),因此 useSwipe 自身无需处理卸载逻辑。

监听器注册在 index.ts:

const listenerOptions = { passive, capture: !passive }

const stops = [
  useEventListener(target, 'touchstart', (e: TouchEvent) => {
    if (e.touches.length !== 1)
      return
    const [x, y] = getTouchEventCoords(e)
    updateCoordsStart(x, y)
    updateCoordsEnd(x, y)
    onSwipeStart?.(e)
  }, listenerOptions),

  useEventListener(target, 'touchmove', (e: TouchEvent) => {
    if (e.touches.length !== 1)
      return
    const [x, y] = getTouchEventCoords(e)
    updateCoordsEnd(x, y)
    if (listenerOptions.capture && !listenerOptions.passive && Math.abs(diffX.value) > Math.abs(diffY.value))
      e.preventDefault()
    if (!isSwiping.value && isThresholdExceeded.value)
      isSwiping.value = true
    if (isSwiping.value)
      onSwipe?.(e)
  }, listenerOptions),

  useEventListener(target, ['touchend', 'touchcancel'], onTouchEnd, listenerOptions),
]

整个生命周期如下:

  1. touchstart:仅当 e.touches.length === 1(单指操作)才记录起点,同时把 coordsEnd 也初始化为起点,保证初始位移为 0,然后触发 onSwipeStart。
  2. touchmove:实时把最新触点写入 coordsEnd;若 passive: false(capture 模式)且横向位移大于纵向,调用 e.preventDefault() 阻止页面随手指滚动;一旦位移越过阈值,isSwiping 置为 true 并开始触发 onSwipe。
  3. touchend / touchcancel:如果当前处于滑动中,则触发 onSwipeEnd(e, direction.value),随后将 isSwiping 复位为 false(见 index.ts)。注意 touchcancel 也被一并监听,因此系统打断手势(如来电、弹窗)时状态也能正确复位。

stop() 的实现是 stops.forEach(s => s())(index.ts),即依次调用三个 useEventListener 返回的清理函数。

passive 与 capture 的作用

这是最容易踩坑的配置,值得单独说明。源码 index.ts 中:

const listenerOptions = { passive, capture: !passive }
  • 默认 passive: true:事件以 passive 模式注册,touchmove 中调用 preventDefault() 会被浏览器忽略(并产生控制台警告),但滚动性能最佳。适合"只需要检测方向、不干预页面滚动"的场景。
  • 设置 passive: false:监听转为 capture 模式,此时 touchmove 中若检测到横向为主轴滑动,会调用 e.preventDefault() 阻止页面横向滚动,实现"滑动卡片时页面不动"的效果。这就是官方演示 demo.vue 将 passive 设为 false 的原因。

使用建议:如果你的滑动区域位于一个本身可纵向滚动的页面中,又需要阻止横向手势干扰滚动,务必设置 passive: false;如果只是被动观察方向,保持默认即可。

实战案例:仿"右滑消除"卡片

仓库中的 demo.vue 是一个完整可运行的实战范例,展示了如何用 useSwipe 实现"右滑卡片消失"交互,核心逻辑如下(节选):

const { direction, isSwiping, lengthX, lengthY } = useSwipe(
  target,
  {
    passive: false,
    onSwipe(e: TouchEvent) {
      if (containerWidth.value) {
        if (lengthX.value < 0) { // 手指向右滑动时 lengthX 为负
          const length = Math.abs(lengthX.value)
          left.value = `${length}px`      // 卡片跟随手指右移
          opacity.value = 1.1 - length / containerWidth.value // 透明度随位移变化
        }
        else {
          left.value = '0'
          opacity.value = 1
        }
      }
    },
    onSwipeEnd(e: TouchEvent, direction: UseSwipeDirection) {
      if (lengthX.value < 0 && containerWidth.value
        && (Math.abs(lengthX.value) / containerWidth.value) >= 0.5) {
        left.value = '100%'  // 位移超过容器一半,直接滑出
        opacity.value = 0
      }
      else {
        left.value = '0'     // 否则回弹复位
        opacity.value = 1
      }
    },
  },
)

这个示例体现了三个可复用的实战要点:

  • 用 isSwiping 控制 CSS transition(模板中 :class="{ animated: !isSwiping }"),滑动过程中关闭过渡以获得跟手效果,松手后启用过渡实现回弹动画;
  • 用 lengthX 的符号判断滑动方向(右滑为负),结合 containerWidth 计算位移比例;
  • 在 onSwipeEnd 中根据"位移是否超过容器一半"决定滑出还是回弹,这是"滑动删除"类交互的通用判定模式。

测试验证:边界行为有据可依

仓库的 index.test.ts 用 Vitest + 模拟 TouchEvent 覆盖了关键边界场景,这些测试结论可以帮助你准确使用 API:

  • 未越过阈值:位移 threshold - 1 时 onSwipe、onSwipeEnd 均不触发,isSwiping 保持 false,direction 为 'none';
  • 越过阈值:位移达到 threshold 后,onSwipe 与 onSwipeEnd 各触发一次;
  • 中途越界又回落:滑动中位移曾越过阈值、结束时又回到阈值内,onSwipe 会多次触发,但 onSwipeEnd 收到的方向是 'none'——即方向以结束时是否越过阈值来最终判定;
  • 方向准确性:up / down / left / right 四种滑动序列均返回预期方向,且 onSwipeEnd 的 direction 参数与返回值一致。

测试还验证了响应式行为:touchstart 后 lengthX、lengthY 均为 0,touchmove 到 (threshold, 5) 后 isSwiping 为 true、direction 为 'right'、lengthX 为 -threshold。如果你在项目中自定义 threshold,这些用例是很好的行为参考。

注意事项

  • 仅支持触摸设备:useSwipe 依赖 TouchEvent,鼠标拖拽不会触发。需要同时支持鼠标的场景,可考虑同包中的 useSwipe 搭配指针事件组合,或参考 useMousePressed 等函数自行封装。
  • 单指约束:touches.length !== 1 时事件会被忽略,多指手势不会污染状态。
  • SSR 安全:事件只在运行时由 useEventListener 挂载到目标元素上,不依赖 window 直接初始化,SSR 场景下只需保证元素引用在客户端可用。
  • lengthX / lengthY 的方向符号:二者均为"起点减终点",与直觉相反,需要右滑正位移时请自行取反。

掌握了上述 API、方向判定逻辑与 passive 配置,你就可以在 Vue 3 项目中快速落地滑动删除、图片轮播、抽屉拖拽等移动端交互,也可以直接阅读 源码实现 与 测试用例 深入理解其设计。

登录后查看全文
vueuse