手写VueUse的useDraggable函数实现

以下是手写的 useDraggable 函数实现,用于实现元素的拖拽功能:

typescript 复制代码
import {
  ref,
  onMounted,
  onUnmounted,
  unref,
  type Ref,
  type ComponentPublicInstance
} from 'vue'

interface UseDraggableOptions {
  initialValue?: { x: number; y: number }
  axis?: 'both' | 'x' | 'y'
  bounds?: {
    left?: number
    right?: number
    top?: number
    bottom?: number
  }
}

export function useDraggable(
  target: Ref<HTMLElement | ComponentPublicInstance | null>,
  options: UseDraggableOptions = {}
) {
  const { initialValue = { x: 0, y: 0 }, axis = 'both', bounds } = options
  const x = ref(initialValue.x)
  const y = ref(initialValue.y)
  const isDragging = ref(false)

  let element: HTMLElement | null = null
  let startX = 0
  let startY = 0
  let initialX = 0
  let initialY = 0

  const getElement = (): HTMLElement | null => {
    const el = unref(target)
    return (el as ComponentPublicInstance)?.$el ?? el
  }

  const applyTransform = () => {
    if (!element) return
    element.style.transform = `translate(${x.value}px, ${y.value}px)`
  }

  const clamp = (value: number, min?: number, max?: number) => {
    if (typeof min === 'number') value = Math.max(value, min)
    if (typeof max === 'number') value = Math.min(value, max)
    return value
  }

  const onStart = (clientX: number, clientY: number) => {
    isDragging.value = true
    startX = clientX
    startY = clientY
    initialX = x.value
    initialY = y.value
  }

  const onMove = (clientX: number, clientY: number) => {
    if (!isDragging.value) return

    let deltaX = clientX - startX
    let deltaY = clientY - startY

    if (axis === 'x') deltaY = 0
    if (axis === 'y') deltaX = 0

    let newX = initialX + deltaX
    let newY = initialY + deltaY

    // 边界限制
    if (bounds) {
      newX = clamp(newX, bounds.left, bounds.right)
      newY = clamp(newY, bounds.top, bounds.bottom)
    }

    x.value = newX
    y.value = newY
    applyTransform()
  }

  const onEnd = () => {
    isDragging.value = false
  }

  const handleMouseDown = (e: MouseEvent) => {
    e.preventDefault()
    onStart(e.clientX, e.clientY)
  }

  const handleTouchStart = (e: TouchEvent) => {
    const touch = e.touches[0]
    if (touch) onStart(touch.clientX, touch.clientY)
  }

  const addEventListeners = () => {
    document.addEventListener('mousemove', handleMouseMove)
    document.addEventListener('mouseup', handleMouseUp)
    document.addEventListener('touchmove', handleTouchMove)
    document.addEventListener('touchend', handleTouchEnd)
  }

  const removeEventListeners = () => {
    document.removeEventListener('mousemove', handleMouseMove)
    document.removeEventListener('mouseup', handleMouseUp)
    document.removeEventListener('touchmove', handleTouchMove)
    document.removeEventListener('touchend', handleTouchEnd)
  }

  const handleMouseMove = (e: MouseEvent) => onMove(e.clientX, e.clientY)
  const handleTouchMove = (e: TouchEvent) => {
    const touch = e.touches[0]
    if (touch) onMove(touch.clientX, touch.clientY)
  }

  const handleMouseUp = () => onEnd()
  const handleTouchEnd = () => onEnd()

  const init = () => {
    element = getElement()
    if (!element) return

    element.style.cursor = 'grab'
    element.style.userSelect = 'none'
    element.style.touchAction = 'none'

    element.addEventListener('mousedown', handleMouseDown)
    element.addEventListener('touchstart', handleTouchStart)
  }

  const cleanup = () => {
    if (!element) return

    element.removeEventListener('mousedown', handleMouseDown)
    element.removeEventListener('touchstart', handleTouchStart)
    element.style.cursor = ''
    element.style.userSelect = ''
    element.style.touchAction = ''
  }

  const stop = () => {
    cleanup()
    removeEventListeners()
  }

  onMounted(() => {
    init()
    applyTransform()
  })

  onUnmounted(() => {
    stop()
  })

  return {
    x,
    y,
    isDragging,
    stop,
    start: () => init(),
    applyTransform
  }
}

使用示例:

vue 复制代码
<template>
  <div ref="draggableEl" class="draggable-box">
    拖拽我
  </div>
  <div class="position-info">
    X: {{ x }}px, Y: {{ y }}px
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { useDraggable } from './useDraggable'

const draggableEl = ref<HTMLElement | null>(null)

const { x, y, isDragging } = useDraggable(draggableEl, {
  initialValue: { x: 100, y: 100 },
  axis: 'both',
  bounds: {
    left: 0,
    top: 0,
    right: window.innerWidth - 200,
    bottom: window.innerHeight - 200
  }
})
</script>

<style>
.draggable-box {
  width: 200px;
  height: 200px;
  background-color: #42b883;
  color: white;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 8px;
  position: fixed;
}

.position-info {
  position: fixed;
  bottom: 20px;
  left: 20px;
  padding: 10px;
  background: rgba(0,0,0,0.7);
  color: white;
  border-radius: 4px;
}
</style>

实现细节说明:

  1. 核心功能

    • 支持鼠标和触摸屏操作
    • 可限制拖拽方向(x/y轴或双向)
    • 支持设置拖拽边界
    • 自动处理CSS transform应用
  2. 响应式数据

    • x: 水平位移
    • y: 垂直位移
    • isDragging: 当前拖拽状态
  3. 实现亮点

    typescript 复制代码
    // 边界限制计算
    const clamp = (value: number, min?: number, max?: number) => {
      if (typeof min === 'number') value = Math.max(value, min)
      if (typeof max === 'number') value = Math.min(value, max)
      return value
    }

    确保元素始终保持在指定边界内

  4. 事件处理优化

    • 使用文档级事件监听器处理移动事件,避免快速拖拽时丢失事件
    • 同时支持鼠标和触摸事件
    typescript 复制代码
    // 统一事件处理
    const onStart = (clientX: number, clientY: number) => {
      // 记录初始位置
    }
    
    const onMove = (clientX: number, clientY: number) => {
      // 计算新位置
    }
  5. 样式处理

    typescript 复制代码
    // 自动应用transform
    const applyTransform = () => {
      if (!element) return
      element.style.transform = `translate(${x.value}px, ${y.value}px)`
    }

    使用transform实现高性能位移动画

  6. 生命周期管理

    typescript 复制代码
    onMounted(() => {
      init()
      applyTransform()
    })
    
    onUnmounted(() => {
      stop()
    })

    确保组件挂载时初始化,卸载时清理资源

  7. 类型安全

    • 完整的TypeScript类型定义
    • 支持Vue组件实例和原生DOM元素

配置选项说明:

参数 类型 默认值 说明
initialValue { x: number; y: number } { x:0, y:0 } 初始位置
axis 'both' | 'x' | 'y' 'both' 允许拖拽的方向
bounds { left?, right?, top?, bottom? } undefined 拖拽边界限制

返回API说明:

属性/方法 类型 说明
x Ref 当前X轴位置
y Ref 当前Y轴位置
isDragging Ref 是否正在拖拽
stop () => void 停止拖拽功能
start () => void 重新启用拖拽功能
applyTransform () => void 立即应用当前transform样式

注意事项:

  1. 目标元素需要设置为 position: fixedposition: absolute
  2. 边界计算需要考虑元素自身的尺寸
  3. 使用transform实现位移,不会影响文档流布局
  4. 触摸事件需要正确处理多点触控场景

该实现提供了与VueUse相似的拖拽功能,同时包含以下增强特性:

  • 更严格的边界限制处理
  • 更完善的触摸事件支持
  • 更直观的API设计
  • 更好的TypeScript类型提示
相关推荐
架构师汤师爷3 分钟前
一文彻底搞懂 OpenClaw 的架构设计与运行原理(万字图文)
前端·agent
苑若轻航4 分钟前
防抖和节流:解决高频事件性能
前端
小黑的铁粉7 分钟前
什么是事件循环?调用堆栈和任务队列之间有什么区别?
前端·javascript
小黑的铁粉7 分钟前
常见的内存泄漏有哪些?
前端·javascript
喝水的长颈鹿8 分钟前
JavaScript 基础入门
前端
喝咖啡的女孩9 分钟前
call、apply、bind 原理与实现
前端
雨落Re9 分钟前
从设计到开发,过年我用十天使用AI搭建了一个完整的博客系统
前端·后端
冴羽19 分钟前
100s 带你了解 Bun 为什么这么火
前端·node.js·bun
Sylvia33.22 分钟前
火星数据:解构斯诺克每一杆进攻背后的数字语言
java·前端·python·数据挖掘·数据分析
Wect40 分钟前
LeetCode 530. 二叉搜索树的最小绝对差:两种解法详解(迭代+递归)
前端·算法·typescript