VueUse usePointerSwipe 实战指南:基于 PointerEvents 的响应式滑动手势检测

原创2026-09-30 13:52:551,369 阅读
文章标签:前端

VueUse usePointerSwipe 实战指南:基于 PointerEvents 的响应式滑动手势检测

导读

usePointerSwipe 是 VueUse 中基于原生 PointerEvents 的响应式滑动手势检测工具,属于核心包 @vueuse/core 的 Sensors(传感器)类别。它统一处理鼠标、触摸屏与触控笔三类指针输入,无需区分 touchstart/touchmove/touchend 与 mousedown/mousemove/mouseup 两套事件体系,即可实现滑动开始、滑动过程、滑动结束的完整手势生命周期追踪。读完本文,你将掌握 usePointerSwipe 的全部配置项与返回值语义、其底层阈值判定与方向计算的源码原理,并能基于它快速实现「滑动删除」「滑动切换卡片」等交互。

usePointerSwipe 是什么

usePointerSwipe 提供基于 PointerEvents 的响应式滑动检测能力,核心源码位于 packages/core/usePointerSwipe/index.ts,并从 packages/core/index.ts 统一导出,可通过 import { usePointerSwipe } from '@vueuse/core' 直接使用。

相比 VueUse 中基于触摸事件的 useSwipe,usePointerSwipe 的优势在于:

  • 依赖 PointerEvent,一套事件体系同时覆盖鼠标(mouse)、触摸(touch)与触控笔(pen)三种指针类型;
  • 通过 setPointerCapture 捕获指针,指针按下后即使移出目标元素,后续的 pointermove 与 pointerup 仍会重定向回目标,滑动判定更稳定;
  • 默认以 passive: true 注册监听器,不阻塞滚动,且自动为目标元素设置 touch-action: pan-y,允许页面纵向滚动、同时拦截横向滑动,避免手势与滚动冲突。

基础用法

官方文档 packages/core/usePointerSwipe/index.md 给出的最小示例:在模板元素上挂 ref,传给组合式函数,即可拿到 isSwiping(是否正在滑动)与 direction(滑动方向):

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

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

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

target 参数类型为 MaybeRefOrGetter<HTMLElement | null | undefined>,即既可以直接传元素引用、ref 对象,也可以传返回元素的 getter 函数。内部通过 toRef(target)(来自 @vueuse/shared)统一归一化为响应式引用,并在元素挂载后自动注册事件监听。

完整 API 与配置项

UsePointerSwipeOptions 接口定义于 packages/core/usePointerSwipe/index.ts,各配置项及默认值如下:

配置项 类型 默认值 说明
threshold number 50 触发滑动判定所需的最小位移(像素),位移未达到该值时不视为滑动
onSwipeStart (e: PointerEvent) => void — 指针按下(pointerdown)时回调
onSwipe (e: PointerEvent) => void — 滑动过程中(pointermove 且已超过阈值)回调
onSwipeEnd (e: PointerEvent, direction: UseSwipeDirection) => void — 滑动结束(pointerup / pointercancel)时回调,携带最终方向
pointerTypes PointerType<a href="https://link.gitcode.com/i/14d7ea3d8825147bda8f2181e68407c4" target="_blank">] ['mouse', 'touch', 'pen'] 允许监听的指针类型,PointerType = 'mouse' | 'touch' | 'pen'(见 [packages/core/types.ts)
disableTextSelect boolean false 是否在滑动时禁用文本选择(设置 user-select: none 系列样式)

返回值 UsePointerSwipeReturn(见 packages/core/usePointerSwipe/index.ts):

返回值 类型 语义
isSwiping Readonly<ShallowRef<boolean>> 当前是否处于滑动状态(位移已超过阈值)
direction Readonly<ShallowRef<UseSwipeDirection>> 当前滑动方向,取值为 'up' | 'down' | 'left' | 'right' | 'none'
posStart DeepReadonly<Position> 滑动起点坐标 { x, y }(clientX/clientY)
posEnd DeepReadonly<Position> 滑动终点坐标 { x, y }
distanceX Readonly<ComputedRef<number>> 水平位移距离(posStart.x - posEnd.x,右滑为负值)
distanceY Readonly<ComputedRef<number>> 垂直位移距离(posStart.y - posEnd.y,下滑为负值)
stop () => void 手动注销全部事件监听

其中 Position 为 { x: number; y: number }(定义于 packages/core/types.ts),UseSwipeDirection 类型则复用自 useSwipe(见 packages/core/useSwipe/index.ts)。

典型带配置示例

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

const el = useTemplateRef('el')

const { isSwiping, direction, distanceX } = usePointerSwipe(el, {
  threshold: 80,                        // 位移 80px 才判定为滑动
  pointerTypes: ['touch', 'mouse'],     // 忽略触控笔输入
  disableTextSelect: true,              // 滑动时禁止选中文本
  onSwipeStart(e) { /* 记录手势开始 */ },
  onSwipe(e) { /* 实时处理位移,如驱动元素跟随 */ },
  onSwipeEnd(e, direction) { /* 根据 direction 决定是否触发操作 */ },
})
</script>

源码级原理:事件链路与方向判定

usePointerSwipe 的完整实现只有约 180 行,逻辑非常紧凑,核心链路如下。

事件监听与指针捕获

函数内部通过 useEventListener(来自 packages/core/useEventListener/index.ts,注册后会在组件卸载时自动移除监听)注册三组事件:

  1. pointerdown:校验事件类型后置 isPointerDown = true,调用 eventTarget.setPointerCapture(e.pointerId) 捕获指针,将 clientX/clientY 同时写入 posStart 与 posEnd(起点等于终点),并触发 onSwipeStart;
  2. pointermove:仅当 isPointerDown 为真时更新 posEnd;若位移首次超过阈值,将 isSwiping 置为 true,此后每次移动触发 onSwipe;
  3. pointerup / pointercancel(同一监听器接收数组形式的多事件):若正处于滑动状态则触发 onSwipeEnd(e, direction.value),随后重置 isPointerDown 与 isSwiping。

所有监听器均以 { passive: true } 注册(见 packages/core/usePointerSwipe/index.ts),保证不阻塞浏览器默认行为。

stop() 的实现即遍历内部保存的 stops 数组逐个注销监听(见 packages/core/usePointerSwipe/index.ts),可用于提前清理。

阈值与方向计算

位移与方向均基于 Vue 的 computed 计算属性实时推导(packages/core/usePointerSwipe/index.ts):

const distanceX = computed(() => posStart.x - posEnd.x)
const distanceY = computed(() => posStart.y - posEnd.y)

const isThresholdExceeded = computed(() =>
  max(abs(distanceX.value), abs(distanceY.value)) >= threshold)

const direction = computed(() => {
  if (!isThresholdExceeded.value)
    return 'none'
  if (abs(distanceX.value) > abs(distanceY.value))
    return distanceX.value > 0 ? 'left' : 'right'
  else
    return distanceY.value > 0 ? 'up' : 'down'
})

方向判定遵循「谁位移大取谁」的原则:水平位移绝对值更大时,向左滑(distanceX > 0)判定为 'left',向右滑判定为 'right';否则按垂直位移判定 'up' / 'down'。位移未达阈值时方向恒为 'none'。

指针类型过滤

eventIsAllowed(packages/core/usePointerSwipe/index.ts)负责事件过滤:

const eventIsAllowed = (e: PointerEvent): boolean => {
  const isReleasingButton = e.buttons === 0
  const isPrimaryButton = e.buttons === 1
  return options.pointerTypes?.includes(e.pointerType as PointerType)
    ?? (isReleasingButton || isPrimaryButton) ?? true
}

即:显式传入 pointerTypes 时,只处理指针类型匹配的事件;未传入时,默认接受主按钮按下(buttons === 1)与按钮释放(buttons === 0)的指针事件。

样式副作用

挂载后(tryOnMounted,来自 @vueuse/shared)会自动对目标元素设置内联样式(packages/core/usePointerSwipe/index.ts):

  • touch-action: pan-y:允许纵向滚动、屏蔽触摸横向滚动,保证横向滑动手势可被捕获;
  • 若 disableTextSelect: true,追加 -webkit-user-select: none、-ms-user-select: none 与 user-select: none,滑动时防止误选中文本。

与 useSwipe(触摸事件方案)的取舍

如果只需要触摸屏手势、不关心鼠标与触控笔,VueUse 还提供了基于 TouchEvent 的 useSwipe。两者对比:

维度 usePointerSwipe useSwipe
事件体系 PointerEvent(鼠标/触摸/触控笔统一) TouchEvent(仅触摸)
方向类型 复用 UseSwipeDirection 定义 UseSwipeDirection 本身
监听选项 固定 passive: true 支持 passive 配置(默认 true),capture: !passive 非被动模式下可调用 preventDefault 拦截横向滚动
指针捕获 使用 setPointerCapture 依赖 touches.length === 1 单指约束
返回结构 posStart/posEnd/distanceX/distanceY coordsStart/coordsEnd/lengthX/lengthY

需要鼠标与触控笔统一输入、或希望跨元素稳定追踪手势时选 usePointerSwipe;需要精细控制事件 passive 行为并在 touchmove 中主动 preventDefault 时选 useSwipe。

实战:结合 distanceX 实现滑动切换

仓库中的 packages/core/usePointerSwipe/demo.vue 提供了一个可直接参考的滑动卡片示例:利用 distanceX 实时驱动元素位移与透明度,并在 onSwipeEnd 中判断滑动距离是否超过容器宽度一半,决定卡片是移出屏幕(left: 100%、opacity: 0)还是回弹复位:

<script setup lang="ts">
import { usePointerSwipe } 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)

const { distanceX, isSwiping } = usePointerSwipe(target, {
  disableTextSelect: true,
  onSwipe(e: PointerEvent) {
    if (containerWidth.value) {
      if (distanceX.value < 0) {                       // 向左滑
        const distance = Math.abs(distanceX.value)
        left.value = `${distance}px`                   // 元素跟随手指位移
        opacity.value = 1.25 - distance / containerWidth.value  // 随位移渐隐
      }
      else {
        left.value = '0'
        opacity.value = 1
      }
    }
  },
  onSwipeEnd(e: PointerEvent, direction) {
    // 位移超过容器一半则滑出屏幕,否则回弹
    if (distanceX.value < 0 && containerWidth.value
      && (Math.abs(distanceX.value) / containerWidth.value) >= 0.5) {
      left.value = '100%'
      opacity.value = 0
    }
    else {
      left.value = '0'
      opacity.value = 1
    }
  },
})
</script>

<template>
  <div ref="container" class="relative w-full h-[80px] overflow-hidden">
    <div ref="target" :class="{ 'transition-all duration-200 ease-linear': !isSwiping }"
      :style="{ left, opacity }">
      Swipe
    </div>
  </div>
</template>

要点提示:distanceX = posStart.x - posEnd.x,因此向左滑为负值、向右滑为正值,与视觉上的元素位移方向相反;滑动过程中应关闭 CSS transition(如示例中仅在 !isSwiping 时启用 transition-all),让元素跟手,滑动结束再借助 transition 平滑回弹或滑出。

测试验证与行为边界

packages/core/usePointerSwipe/index.browser.test.ts 用 Vitest + 模拟 PointerEvent 对行为做了完整验证,可作为理解 API 语义的权威参照:

  • 未超阈值:位移小于 threshold 时,onSwipeStart 触发 1 次,onSwipe 与 onSwipeEnd 均不触发(threshold is not exceeded 用例);
  • 超过阈值:onSwipeStart / onSwipe / onSwipeEnd 各触发一次,onSwipeEnd 携带方向 'right';
  • 中途回退:曾超过阈值后又回到阈值内,onSwipe 只在中途触发,结束时 onSwipeEnd 携带方向 'none'(isSwiping 仍会先置真);
  • 响应式:pointerdown 后 isSwiping 为 false、direction 为 'none';pointermove 超阈值后 isSwiping 变真、direction 更新为 'right';pointerup 后 isSwiping 复位为 false;
  • 类型过滤:设置 pointerTypes: ['touch'] 后派发鼠标指针事件不产生任何响应;
  • 方向矩阵:up / down / left / right 四组坐标用例均被正确识别,且 onSwipeEnd 收到对应方向;
  • stop 清理:调用 stop() 后继续派发移动事件不再触发任何响应。

从这些用例可以总结出三个容易被忽略的边界行为:滑动结束后 direction 不会立即清空(保留最后一次判定结果,如 'right');手势中途回退到阈值内时结束方向为 'none';在未按下指针时派发 pointermove 会被忽略。

小结

usePointerSwipe 以极小的实现体量(单个 180 行左右的 index.ts)封装了基于 PointerEvents 的完整滑动检测能力:统一的指针类型过滤、指针捕获、阈值判定、方向计算与生命周期回调一应俱全。配合 distanceX / distanceY 响应式位移和 isSwiping 状态,即可低成本实现滑动删除、滑动分页、手势导航等常见移动端与桌面端交互;需要调整触发灵敏度时,只需修改 threshold,需要限定输入设备时,配置 pointerTypes 即可。

登录后查看全文
vueuse