VueUse useSwipe 全面指南:基于 TouchEvent 的响应式滑动方向检测
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>
注意两点:
target参数的类型是MaybeRefOrGetter<EventTarget | null | undefined>,即可以传入模板引用、ref、getter函数,甚至直接传入元素本身;配合 Vue 3.5+ 的useTemplateRef使用最为自然。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'
}
})
判定规则可以归纳为三步:
- 阈值门禁:
max(|diffX|, |diffY|) >= threshold才继续,否则一律返回'none'(这一判断对应isThresholdExceeded计算属性,见 index.ts)。这避免了手指轻微抖动被误判为滑动。 - 主轴选择:比较
|diffX|与|diffY|,位移更大的方向为主轴,即使手指轨迹是斜线也只会得到一个主轴方向。 - 符号定方向:在主轴上,位移为正是因为起点坐标大于终点坐标(手指向左 / 向上),分别得到
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),
]
整个生命周期如下:
- touchstart:仅当
e.touches.length === 1(单指操作)才记录起点,同时把coordsEnd也初始化为起点,保证初始位移为 0,然后触发onSwipeStart。 - touchmove:实时把最新触点写入
coordsEnd;若passive: false(capture 模式)且横向位移大于纵向,调用e.preventDefault()阻止页面随手指滚动;一旦位移越过阈值,isSwiping置为true并开始触发onSwipe。 - 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控制 CSStransition(模板中: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 项目中快速落地滑动删除、图片轮播、抽屉拖拽等移动端交互,也可以直接阅读 源码实现 与 测试用例 深入理解其设计。