VueUse useSwipe 组合式函数指南:基于 TouchEvent 的响应式滑动手势检测

原创2026-10-05 19:47:241,429 阅读
文章标签:前端

VueUse useSwipe 组合式函数指南:基于 TouchEvent 的响应式滑动手势检测

导读

useSwipe 是 VueUse 核心包(@vueuse/core)中位于 Sensors(传感器) 类别下的一个组合式函数,它基于浏览器原生 TouchEvent、核心实现、官方示例 与 单元测试,完整讲解 useSwipe 的 API、方向判定算法、阈值机制、事件生命周期回调,以及如何在 Vue 3 组件中用它构建"滑动删除"等真实交互场景。

一、useSwipe 是什么

useSwipe 提供基于 TouchEvent 的响应式滑动检测。在移动端(触摸屏)交互中,滑动(Swipe)是最常见的手势之一:列表项滑动删除、图片轮播切换、卡片滑动关闭、抽屉面板手势收起等场景都依赖它。

它天然支持 Vue 3 的响应式体系,返回值中 isSwiping、direction、lengthX、lengthY 均为可响应状态,滑动过程中 UI 可以实时响应手势变化。在 VueUse 仓库中,它被归类为 packages/core/useSwipe,同类别(Sensors)下还有 usePointerSwipe(基于 Pointer Events 的滑动检测)等,但 useSwipe 是专为触屏 TouchEvent 设计的。

二、安装与引入

useSwipe 从 @vueuse/core 包导出:

import { useSwipe } from '@vueuse/core'

三、基本用法

3.1 最小示例

<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>

这是 skills/vueuse-functions/references/useSwipe.md 中的标准用法:

  1. 通过 Vue 3.5+ 的 useTemplateRef 拿到模板 ref;
  2. 将其作为 target 传入 useSwipe;
  3. 返回解构出 isSwiping(是否正在滑动)与 direction(滑动方向)。

3.2 完整示例:滑动删除卡片

官方 demo.vue 展示了一个接近真实产品的"左滑删除"场景,可完整复制运行:

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

const target = useTemplateRef('target')
const container = useTemplateRef('container')
const containerWidth = computed(() => container.value?.offsetWidth)
const left = shallowRef('0')
const opacity = shallowRef(1)

function reset() {
  left.value = '0'
  opacity.value = 1
}

const { direction, isSwiping, lengthX, lengthY } = useSwipe(
  target,
  {
    passive: false,
    onSwipe(e: TouchEvent) {
      if (containerWidth.value) {
        if (lengthX.value < 0) {
          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
      }
    },
  },
)
</script>

<template>
  <div>
    <div ref="container" class="container select-none">
      <button @click="reset">
        Reset
      </button>
      <div ref="target" class="overlay" :class="{ animated: !isSwiping }" :style="{ left, opacity }">
        <p>Swipe right</p>
      </div>
    </div>
    <p class="status">
      Direction: {{ direction ? direction : '-' }} <br>
      lengthX: {{ lengthX }} | lengthY: {{ lengthY }}
    </p>
  </div>
</template>

<style scoped>
.container {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 2px dashed #ccc;
  overflow: hidden;
}

.overlay {
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  position: absolute;
  background: #3fb983;
}

.overlay.animated {
  transition: all 0.2s ease-in-out;
}

.overlay > p {
  color: #fff;
  font-weight: bold;
  text-align: center;
  overflow: hidden;
  white-space: nowrap;
}

.status {
  text-align: center;
}
</style>

该示例的核心思路:

  • 跟随手势:onSwipe 回调中通过 lengthX(响应式计算的位移量)实时设置元素 left 偏移,并随滑出距离等比降低 opacity,让元素"粘在手指上";
  • 判定删除:onSwipeEnd 中当 lengthX < 0(左滑)且滑出距离达到容器宽度 50% 时,将元素完全滑出并隐藏(opacity: 0),否则回弹复位;
  • isSwiping 驱动动画:滑动过程中通过 :class="{ animated: !isSwiping }" 关闭 CSS 过渡(避免手势跟手延迟),滑动结束后开启 transition: all 0.2s ease-in-out 实现平滑回弹/滑出动画。

注意:示例将 passive 设为 false,这是为了在 touchmove 中调用 preventDefault() 阻止浏览器默认滚动行为(详见下文"passive 选项"与源码分析)。

四、完整 API 与类型声明

useSwipe 的完整类型签名如下(来自 skills/vueuse-functions/references/useSwipe.md,与 packages/core/useSwipe/index.ts 的实现一一对应):

export type UseSwipeDirection = "up" | "down" | "left" | "right" | "none"

export interface UseSwipeOptions extends ConfigurableWindow {
  /**
   * Register events as passive
   *
   * @default true
   */
  passive?: boolean
  /**
   * @default 50
   */
  threshold?: number
  /**
   * Callback on swipe start
   */
  onSwipeStart?: (e: TouchEvent) => void
  /**
   * Callback on swipe moves
   */
  onSwipe?: (e: TouchEvent) => void
  /**
   * Callback on swipe ends
   */
  onSwipeEnd?: (e: TouchEvent, direction: UseSwipeDirection) => void
}

export interface UseSwipeReturn {
  isSwiping: ShallowRef<boolean>
  direction: ComputedRef<UseSwipeDirection>
  coordsStart: Readonly<Position>
  coordsEnd: Readonly<Position>
  lengthX: ComputedRef<number>
  lengthY: ComputedRef<number>
  stop: () => void
}

export declare function useSwipe(
  target: MaybeRefOrGetter<EventTarget | null | undefined>,
  options?: UseSwipeOptions,
): UseSwipeReturn

4.1 函数签名

function useSwipe(
  target: MaybeRefOrGetter<EventTarget | null | undefined>,
  options?: UseSwipeOptions,
): UseSwipeReturn
  • target:MaybeRefOrGetter<EventTarget | null | undefined>,即可以传入 DOM 元素、模板 ref、ref 对象或 getter 函数。源码中通过 useEventListener 内部对目标进行响应式解析(包括 unrefElement 处理),因此即使元素在异步渲染后才挂载也能正确绑定。
  • options:可选配置,详见下表。

4.2 选项参数表

选项 类型 默认值 说明
passive boolean true 是否以 passive 模式注册 touch 事件监听。若为 false,则改为 capture(捕获)模式注册,且允许在 touchmove 中调用 preventDefault() 阻止滚动
threshold number 50 位移阈值(像素)。手指从起点移动超过该阈值才认定为滑动
onSwipeStart (e: TouchEvent) => void — 手指按下(touchstart)时的回调
onSwipe (e: TouchEvent) => void — 手指移动(touchmove)且已进入滑动状态时的回调
onSwipeEnd (e: TouchEvent, direction: UseSwipeDirection) => void — 手指抬起/取消(touchend / touchcancel)时的回调,第二参数为该次滑动的最终方向

UseSwipeOptions 还继承了 ConfigurableWindow(packages/core/_configurable.ts),可通过 window 选项传入自定义 window 实例(如 iframe 或测试环境),该接口同样被 VueUse 大量组合式函数复用。

4.3 返回值

返回值 类型 说明
isSwiping ShallowRef<boolean> 是否正在滑动(位移已超过阈值且手指未离开)
direction ComputedRef<UseSwipeDirection> 当前/最终滑动方向:`'up'
coordsStart Readonly<Position> 手势起点坐标(x、y,取自 touchstart 的 clientX/clientY)
coordsEnd Readonly<Position> 手势当前/终点坐标(每次 touchmove 更新)
lengthX ComputedRef<number> 横向位移:coordsStart.x - coordsEnd.x
lengthY ComputedRef<number> 纵向位移:coordsStart.y - coordsEnd.y
stop () => void 手动解绑全部三个事件监听器(touchstart / touchmove / touchend+touchcancel)

其中 Position 类型定义于 packages/core/types.ts:

export interface Position {
  x: number
  y: number
}

coordsStart、coordsEnd 是 reactive 对象,lengthX/lengthY/direction 都是 computed,因此滑动过程中的每一帧位移变化都会驱动 UI 响应式更新。

五、源码级原理解析

5.1 状态初始化与默认值

实现源码 开头解构选项并建立核心状态:

const {
  threshold = 50,
  onSwipe,
  onSwipeEnd,
  onSwipeStart,
  passive = true,
} = options

const coordsStart = reactive<Position>({ x: 0, y: 0 })
const coordsEnd = reactive<Position>({ x: 0, y: 0 })

const diffX = computed(() => coordsStart.x - coordsEnd.x)
const diffY = computed(() => coordsStart.y - coordsEnd.y)

const { max, abs } = Math
const isThresholdExceeded = computed(() => max(abs(diffX.value), abs(diffY.value)) >= threshold)

const isSwiping = shallowRef(false)

注意两点:

  1. 位移方向约定:diffX = coordsStart.x - coordsEnd.x。手指从左向右滑(coordsEnd.x 增大)时 diffX 为负;向右滑返回 direction: 'right'、lengthX 为负。这与 demo 中"lengthX < 0 视为左滑"的用法一致(测试 reactivity 用例中 lengthX 为 -threshold 时 direction 为 'right',见 index.test.ts)。
  2. 阈值比较使用横、纵位移绝对值的最大值:max(abs(diffX), abs(diffY)) >= threshold,保证对角线滑动同样能被触发,且方向判定会优先采用位移较大的轴。

5.2 方向判定算法

方向计算 逻辑如下:

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. 未超过阈值时方向恒为 'none';
  2. 超过阈值后,比较横纵位移绝对值:
    • |diffX| > |diffY| 判为水平方向,再根据 diffX 正负得出 left(手指左滑)/ right(手指右滑);
    • 否则判为垂直方向,根据 diffY 正负得出 up / down。

该算法由测试用例完整覆盖,index.test.ts 中 it.each 分别验证了四个方向的坐标序列与最终方向、onSwipeEnd 第二参数的一致性。

5.3 事件生命周期与监听器

整个组合式函数围绕三个事件构建(源码 L106-L141):

const listenerOptions = { passive, capture: !passive }

const onTouchEnd = (e: TouchEvent) => {
  if (isSwiping.value)
    onSwipeEnd?.(e, direction.value)

  isSwiping.value = false
}

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),
]

const stop = () => stops.forEach(s => s())

要点解析:

  • 单指约束:touchstart/touchmove 都会先检查 e.touches.length !== 1,多指触控直接忽略,避免多点触控干扰手势判定。
  • 坐标来源:getTouchEventCoords 读取 e.touches<a href="https://link.gitcode.com/i/c9140ac9b33201ecfbcc5323ba114168" target="_blank">0].clientX/clientY([源码 L94)。touchstart 同时更新起点与终点,之后每次 touchmove 只更新终点,从而得到实时位移。
  • isSwiping 的触发时机:touchmove 中当位移首次超过阈值(isThresholdExceeded)时才将 isSwiping 置为 true,此后每次移动都会调用 onSwipe 回调。
  • touchend 与 touchcancel 统一处理:touchend(正常抬起)与 touchcancel(系统中断,如来电)都会触发 onTouchEnd,仅当正处于滑动状态时回调 onSwipeEnd(e, direction.value),随后重置 isSwiping = false。
  • 生命周期自动管理:三个监听器均通过 useEventListener 注册,它内部使用 watchImmediate 在目标解析后绑定、组件卸载时自动解绑(packages/core/useEventListener/index.ts),因此无需手动清理,除非调用 stop()。

5.4 passive 选项的深层影响

监听器配置为 { passive, capture: !passive }:

  • passive: true(默认):以 passive 模式注册监听器,touchmove 中无法调用 preventDefault(),但浏览器滚动/缩放行为不被阻塞,性能最优。适合"只检测方向、不拦截滚动"的轻量场景。
  • passive: false:改为 capture(捕获)阶段 注册,此时源码中的这段逻辑生效:
if (listenerOptions.capture && !listenerOptions.passive && Math.abs(diffX.value) > Math.abs(diffY.value))
  e.preventDefault()

即在横向位移占优(|diffX| > |diffY|)时主动 preventDefault() 阻止页面垂直滚动,避免手势跟手时页面滚动冲突。这正是 demo 中"左滑删除卡片"必须设置 passive: false 的原因。

六、测试驱动的行为验证

packages/core/useSwipe/index.test.ts 使用 Vitest + jsdom 模拟 TouchEvent 分发,从测试用例中可以反推得到一组精确的行为契约:

测试场景 行为验证
threshold not exceeded(位移未达阈值) onSwipe 与 onSwipeEnd 均不触发,方向保持 none
threshold exceeded(位移超阈值) onSwipe 与 onSwipeEnd 各触发一次
threshold exceeded in between(中途回移低于阈值) onSwipe 触发 2 次,onSwipeEnd 的 direction 为 none——即抬起时位移不足阈值则方向归零
reactivity isSwiping、direction、lengthX、lengthY 随 touchstart/touchmove 实时响应(如右滑 30px 后 lengthX 为 -30、direction 为 'right')
swipe up/down/left/right(四方向参数化用例) 每个方向的位移序列都正确产出对应方向,且 onSwipeEnd 第二参数与之一致

对 direction 的完整行为契约可以总结为:

  • 未达阈值(max(|diffX|, |diffY|) < threshold):'none';
  • 达到阈值且 |diffX| > |diffY|:diffX > 0 为 'left',否则 'right';
  • 达到阈值且 |diffX| <= |diffY|:diffY > 0 为 'up',否则 'down'。

七、在 Vue 3 组件中的集成要点

7.1 target 的三种传法

  • 模板 ref:const el = useTemplateRef('el'); useSwipe(el)(Vue 3.5+ 推荐写法);
  • 普通 ref:const el = ref<HTMLElement | null>(null); useSwipe(el);
  • getter:useSwipe(() => document.querySelector('.swipe-area'))。

三者都满足 MaybeRefOrGetter<EventTarget | null | undefined> 签名,元素在异步渲染完成后挂载也能被 useEventListener 响应式捕获。

7.2 与指令/组件体系的对比

useSwipe 属于组合式 API 形态,需要搭配模板 ref 使用。仓库中 onClickOutside、onKeyStroke 等同时提供指令形态(directive.ts),但 useSwipe 目前仅提供组合式函数形态,更适合在 <script setup> 中声明式组合。

7.3 适用边界

  • 仅 TouchEvent:useSwipe 基于 Touch API,桌面端鼠标拖拽无法触发。若需兼容鼠标与触屏(Pointer Events),可参考同仓库的 usePointerSwipe(packages/core/usePointerSwipe/index.ts);
  • 浏览器能力:需要支持 TouchEvent 的运行环境(移动端浏览器、触屏笔记本),桌面 Chrome DevTools 的设备模拟模式也可调试;
  • SSR 注意:TouchEvent 属于浏览器 API,组合式函数应在 onMounted 之后的客户端交互中体现效果;VueUse 的 SSR 安全模式(isClient 判断,见 packages/core/_configurable.ts)会避免服务端注册监听器。

八、从文档到源码的快速导航

若想深入研读,本仓库内可依次查看:

资源 路径 说明
API 参考文档 skills/vueuse-functions/references/useSwipe.md 含完整类型声明
组合式函数文档 packages/core/useSwipe/index.md 官方基础用法
核心实现 packages/core/useSwipe/index.ts 方向算法、事件绑定、默认值
官方演示 packages/core/useSwipe/demo.vue 可运行的滑动删除示例
单元测试 packages/core/useSwipe/index.test.ts 阈值、方向、响应性行为契约
事件监听基础 packages/core/useEventListener/index.ts 监听器自动绑定/解绑原理
通用类型 packages/core/types.ts Position 定义

九、小结

useSwipe 用极简的 API(一个目标 + 三个回调 + 四个响应式状态)封装了完整的触摸滑动检测链路:

  • 以 touchstart 记录起点、touchmove 追踪终点并驱动响应式位移;
  • 以 max(|diffX|, |diffY|) >= threshold 作为滑动判定门槛,默认 50px;
  • 以位移绝对值比较 + 正负号完成四方向判定;
  • 以 touchend/touchcancel 统一结束手势并回传最终方向;
  • 通过 passive 选项控制是否在捕获阶段 preventDefault() 以协调页面滚动。

无论是需要"滑动跟手 + 滑出删除"的高交互场景,还是仅需轻量方向检测的场景,都可以直接用 useSwipe 一行接入,并在 onSwipe/onSwipeEnd 回调中完成业务逻辑。

登录后查看全文
vueuse