大家好,这是我开设第一个系列文章,旨在提升自己的源码阅读和调试能力、学习更多的优秀代码与逻辑。
如何调试
- 克隆最新的 VueUse 仓库
git clone https://github.com/vueuse/vueuse.git
- 安装依赖,因为依赖管理器配置的是
pnpm
,所以使用pnpm
来安装依赖pnpm install
- 启动项目
pnpm dev
- 如果没有意外的话,就能在浏览器上看到运行在本地的官方文档页面
- 在
/packages/core
文件夹下找到想要调试的目标方法,在 VSCode 和浏览器源代码中设置断点
接下来就可以开始愉快的调试了!
useDraggable
让元素变得可拖动,有两种使用方式:
- Hook – 源码位置
/useDraggable/index.ts
- Component – 源码位置
/useDraggable/component.ts
Hook 方式
使用示例
通过useDraggable
返回的style
去改变元素样式来实现拖拽效果
<script setup lang="ts">
import { ref } from 'vue'
import { useDraggable } from '@vueuse/core'
const el = ref<HTMLElement | null>(null)
const { x, y, style } = useDraggable(el, {
initialValue: { x: 80, y: 80 },
preventDefault: true,
exact: true,
})
</script>
<template>
<div
ref="el"
:style="style"
>
👋 Drag me!
<div>
I am at {{ Math.round(x) }}, {{ Math.round(y) }}
</div>
</div>
</template>
参数说明
export function useDraggable(
target: MaybeRefOrGetter<HTMLElement | SVGElement | null | undefined>,
options: UseDraggableOptions = {},
) {
...
}
useDraggable 接受两个参数
target
– 拖拽的目标元素option
– 可选参数,UseDraggableOptions
类型,下面贴出源码来解释
export interface UseDraggableOptions {
/**
* Only start the dragging when click on the element directly
* 只在直接点击元素时才开始拖动
*
* @default false
*/
exact?: MaybeRefOrGetter<boolean>
/**
* Prevent events defaults
* 阻止事件默认行为
*
* @default false
*/
preventDefault?: MaybeRefOrGetter<boolean>
/**
* Prevent events propagation
* 阻止事件传播
*
* @default false
*/
stopPropagation?: MaybeRefOrGetter<boolean>
/**
* Whether dispatch events in capturing phase
* 是否在捕获阶段调度事件
*
* @default true
*/
capture?: boolean
/**
* Element to attach `pointermove` and `pointerup` events to.
* 将'pointintermove'和'pointinterup'事件附加到哪个元素上
*
* @default window
*/
draggingElement?: MaybeRefOrGetter<HTMLElement | SVGElement | Window | Document | null | undefined>
/**
* Handle that triggers the drag event
* 触发拖动事件的元素
*
* @default target
*/
handle?: MaybeRefOrGetter<HTMLElement | SVGElement | null | undefined>
/**
* Pointer types that listen to.
* 点击类型
*
* @default ['mouse', 'touch', 'pen']
*/
pointerTypes?: PointerType[]
/**
* Initial position of the element.
* 元素初始位置
*
* @default { x: 0, y: 0 }
*/
initialValue?: MaybeRefOrGetter<Position>
/**
* Callback when the dragging starts. Return `false` to prevent dragging.
* 拖动开始时的回调。返回'false'以防止拖动
*/
onStart?: (position: Position, event: PointerEvent) => void | false
/**
* Callback during dragging.
* 拖动时的回调
*/
onMove?: (position: Position, event: PointerEvent) => void
/**
* Callback when dragging end.
* 拖动结束时的回调
*/
onEnd?: (position: Position, event: PointerEvent) => void
/**
* Axis to drag on.
* 可拖动的方向
*
* @default 'both'
*/
axis?: 'x' | 'y' | 'both'
}
代码解析
参数解构
const {
pointerTypes,
preventDefault,
stopPropagation,
exact,
onMove,
onEnd,
onStart,
initialValue,
axis = 'both',
draggingElement = defaultWindow,
handle: draggingHandle = target,
} = options
有个地方需要提下,因为刚开始看得时候我是一脸懵逼,就是const { handle: draggingHandle = target } = options
,它的意思是从options
对象里面取出handle
属性,并将其赋值给draggingHandle
,如果options
中没有handle
属性,那么draggingHandle
变量就是target
,也就是useDraggable
的第一个参数
注册事件监听器
if (isClient) {
// 处理事件监听参数
const config = { capture: options.capture ?? true }
// draggingHandle 默认为 target,它的主要作用是用来控制在哪个元素上触发点击事件时开始拖拽行为
useEventListener(draggingHandle, 'pointerdown', start, config)
useEventListener(draggingElement, 'pointermove', move, config)
useEventListener(draggingElement, 'pointerup', end, config)
}
在这里需要理解draggingHandle
和draggingElement
变量,以及为什么是在draggingHandle
监听pointerdown
事件,在draggingElement
监听pointermove
和pointerup
。
- draggingHandle – 在上面参数中有讲解,它的值为
options.handle || taget
,这个变量的作用就是用来控制在哪个元素上触发点击事件时开始拖拽行为。 - draggingElement – 它的值为
options.draggingElement || window
,因为在它上面绑定了pointermove
和pointerup
事件监听器,用来计算拖动位置和监听拖拽结束的,那这个变量的作用用通俗的话来说就是鼠标在哪个元素内移动时可以进行元素的拖拽。
关于isClient
变量,源码为const isClient = typeof window !== 'undefined'
,用来判断当前环境是否为浏览器。
关于useEventListener
先不做过多的赘述,它是VueUse
对ele.addEventListener
和ele.removeEventListener
进行的一层封装,接受四个参数:
- target – 创建事件监听器的目标元素
- event – 事件名称
- listener – 事件触发时执行的函数
- options – 此参数同
[addEventListener](https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget/addEventListener#%E8%AF%AD%E6%B3%95)
的第三个参数
关于pointerdown/pointermove/pointerup
和mousedown/mousemove/mouseup
的区别,它们都可以用来处理鼠标和触摸屏等指针设备的事件,但是mousedown/mousemove/mouseup
是只支持鼠标事件,而pointerdown/pointermove/pointerup
支持多种不同的指针设备,包括触摸屏、触控笔等等。
实现拖拽
元素拖拽的功能逻辑主要在start()
move()
end()
这三个函数中,在进入这上个函数之间去需要先了解几个通用函数
toValue()
– 函数意图很清晰,如何参数是函数的话就返回函数的返回值,不是函数的话就调用 Vue 中的unref()
函数并返回。
export function toValue<T>(r: MaybeRefOrGetter<T>): T {
return typeof r === 'function'
? (r as AnyFn)()
: unref(r)
}
filterEvent()
– 判断当前触发的事件类型是否在useDraggable
参数options.pointerTypes
之中,不传options
时默认为['mouse', 'touch', 'pen']
。
const filterEvent = (e: PointerEvent) => {
if (pointerTypes)
return pointerTypes.includes(e.pointerType as PointerType)
return true
}
handleEvent()
– 根据useDraggable
参数options.pointerTypes
和options.stopPropagation
来是否阻止事件默认行为和事件传播。
const handleEvent = (e: PointerEvent) => {
if (toValue(preventDefault))
e.preventDefault()
if (toValue(stopPropagation))
e.stopPropagation()
}
下面解析拖拽的主要逻辑函数start()
、move()
、end()
start()
函数
const start = (e: PointerEvent) => {
// 判断是否在配置的事件类型之内
if (!filterEvent(e))
return
// 当exact为true时,判断点击事件触发的元素是否和需要拖拽的元素是否一致,如若不一致,则不能拖动
if (toValue(exact) && e.target !== toValue(target))
return
// 获取需要拖拽的元素的位置坐标
const rect = toValue(target)!.getBoundingClientRect()
// 计算点击位置与拖动元素左上角坐标的差值
const pos = {
x: e.clientX - rect.left,
y: e.clientY - rect.top,
}
// 判断是否定义了拖动开始时的回调函数以及回调函数是否返回的false
if (onStart?.(pos, e) === false)
return
// 保存点击位置与元素坐标的差值
pressedDelta.value = pos
// 控制指针点击事件的默认行为与传播
handleEvent(e)
}
move()
函数
// 记录元素坐标
const position = ref<Position>(
toValue(initialValue) ?? { x: 0, y: 0 },
)
const move = (e: PointerEvent) => {
// 判断是否在配置的事件类型之内
if (!filterEvent(e))
return
// 判断开始移动以前是否触发过点击事件start()方法
if (!pressedDelta.value)
return
let { x, y } = position.value
// axis为x或both时,元素可以在x轴上拖动
if (axis === 'x' || axis === 'both')
// 计算元素x轴坐标
x = e.clientX - pressedDelta.value.x
// axis为y或both时,元素可以在y轴上拖动
if (axis === 'y' || axis === 'both')
// 计算元素y轴坐标
y = e.clientY - pressedDelta.value.y
// 更新元素坐标
position.value = {
x,
y,
}
// 判断是否定义了拖动时的回调函数,若定义了则执行回调函数
onMove?.(position.value, e)
// 控制指针移动事件的默认行为与传播
handleEvent(e)
}
end()
函数
const end = (e: PointerEvent) => {
// 判断是否在配置的事件类型之内
if (!filterEvent(e))
return
// 判断是否触发过点击事件start()方法
if (!pressedDelta.value)
return
// 将坐标差值数据清空,因为后面需要使用这个变量来判断元素是否在拖动构成中
pressedDelta.value = undefined
// 判断是否定义了拖动结束时的回调函数,若定义了则执行回调函数
onEnd?.(position.value, e)
// 控制指针释放事件的默认行为与传播
handleEvent(e)
}
返回值
export function useDraggable(
target: MaybeRefOrGetter<HTMLElement | SVGElement | null | undefined>,
options: UseDraggableOptions = {},
) {
...
return {
// 返回元素左上角最新坐标值:x, y
...toRefs(position),
// 返回Ref类型的元素左上角坐标
position,
// 用于判断元素是否在拖动过程中
isDragging: computed(() => !!pressedDelta.value),
// 元素样式,通过left和top来控制元素位置,直接把它绑定到元素上即可使用拖动
style: computed(
() => `left:${position.value.x}px;top:${position.value.y}px;`,
),
}
}
至此,直接将useDraggable
返回的style
绑定到元素上即可实现拖动效果,VueUse
里面默认使用的是left
和top
的方式来使用元素位置的改变,我们可以通过返回的position
去使用其他方式来改变元素位置。
Component 方式
使用示例
将需要拖拽的组件放到<UseDraggable></UseDraggable>
内即可
<script setup lang="ts">
import { ref } from 'vue'
import { UseDraggable } from '@vueuse/components'
</script>
<template>
<UseDraggable
v-slot="{ x, y }"
p="x-4 y-2"
:initial-value="{ x: 150, y: 150 }"
:prevent-default="true"
storage-key="vueuse-draggable-pos"
storage-type="session"
>
Renderless component
<div>
Position persisted in sessionStorage
</div>
<div>
{{ Math.round(x) }}, {{ Math.round(y) }}
</div>
</UseDraggable>
</template>
参数说明
以组件的方式使用时,里面默认集成了持久化保存坐标数据的功能,参数基本上和useDraggable
的中options
大部分一致,有几个是options
没有的:
- storageType – 用来持久化保存坐标数据的位置,为
'session'
时,保存在sessionStorage
,否则保存在localStorage
中 - storageKey – 保存在
sessionStorage
或localStorage
中的键名 - as – 渲染时使用什么标签包裹
<UseDraggable></UseDraggable>
内部的元素
代码解析
持久化数据
// 如何传递了storageKey参数,就去对应的缓存位置拿取数据
const storageValue = props.storageKey && useStorage(
props.storageKey,
toValue(props.initialValue) || { x: 0, y: 0 },
isClient
? props.storageType === 'session'
? sessionStorage
: localStorage
: undefined,
)
// 获取初始坐标数据
const initialValue = storageValue || props.initialValue || { x: 0, y: 0 }
// 拖拽结束时保存数据
const onEnd = (position: Position) => {
if (!storageValue)
return
storageValue.value.x = position.x
storageValue.value.y = position.y
}
关于useStorage
先不做了解,在后续的文章再解析,现在理解成根据storageType
和storageKey
去浏览器缓存保存和获取数据
调用useDraggable以及渲染模板
<UseDraggable>
组件内部其实也是调用了useDraggable
,将<UseDraggable>
组件上的参数经过处理后调用useDraggable
,下面我把持久化坐标功能代码剔除后来讲解
export const UseDraggable = defineComponent<UseDraggableProps>({
name: 'UseDraggable',
props: [
'storageKey',
'storageType',
'initialValue',
'exact',
'preventDefault',
'stopPropagation',
'pointerTypes',
'as',
'handle',
'axis',
] as unknown as undefined,
setup(props, { slots }) {
// 声明一个ref,后面渲染的组件会绑定ref,用于获取组件实例
const target = ref()
// 用来控制在哪个元素上触发点击事件时开始拖拽行为。
// 类似于 useDraggable 中的 const { handle: draggingHandle = target } = options
const handle = computed(() => props.handle ?? target.value)
// 处理元素初始坐标
const initialValue = props.initialValue || { x: 0, y: 0 }
// 调用 useDraggable,这部分的逻辑就跟上面一致了
const data = reactive(useDraggable(target, {
...props,
handle,
initialValue,
onEnd,
}))
return () => {
// 判断 <UseDraggable> 组件内部是否有内容
if (slots.default)
return h(props.as || 'div', { ref: target, style: `touch-action:none;${data.style}` }, slots.default(data))
}
},
})
关于最后的渲染函数h()
:
- 第一个参数
props.as || 'div'
是用来指定渲染时使用什么标签; - 第二个参数
{ ref: target, style:
touch-action:none;${data.style}}
,则是将参数里的属性直接设置在渲染出的元素上; - 第三个参数
slots.default(data)
,就是在渲染结果中加入默认插槽,并将data
里的数据作为插槽的参数,可以转换为<slots v-bind="data"></slot>
,没有显式指定name属性时,默认为default
。
渲染元素最后是把ref="target"
加上了,这样useDraggable
的target
参数就是渲染出来的元素,指定了拖拽的目标元素
使用示例解析
<script setup lang="ts">
import { ref } from 'vue'
import { UseDraggable } from '@vueuse/components'
</script>
<template>
<UseDraggable
v-slot="{ x, y }"
p="x-4 y-2"
:initial-value="{ x: 150, y: 150 }"
:prevent-default="true"
storage-key="vueuse-draggable-pos"
storage-type="session"
>
Renderless component
<div>
Position persisted in sessionStorage
</div>
<div>
{{ Math.round(x) }}, {{ Math.round(y) }}
</div>
</UseDraggable>
</template>
到这里整个useDraggable
的源码解析结束了,两个使用方式各有各的特点。
PS:使用时一定不要忘了给需要拖动的元素加上**postion:fixed**
,否则不生效哦!!!
两种使用方式的差异
- Hook 方式更加的灵活,可以通过
onMove
、onEnd
和onStart
这三个回调函数加入更多的逻辑; - Component 方式更加简单,集成持久化拖拽数据、不用显式指定
target
。
使用 transform 替代 left 和 top
因为left
和top
的修改会频繁的触发重排重绘,基于性能考虑我们将其改成transform
形式
Hook
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useDraggable } from '@vueuse/core'
const el = ref<HTMLElement | null>(null)
const { x, y, style } = useDraggable(el, {
initialValue: { x: 80, y: 80 },
preventDefault: true,
exact: true,
})
const transformStyle = computed(() => `transform: translate(${position.value.x}px, ${position.value.y}px)`)
</script>
<template>
<div
ref="el"
:style="transformStyle"
>
👋 Drag me!
<div>
I am at {{ Math.round(x) }}, {{ Math.round(y) }}
</div>
</div>
</template>
Component
因为<UseDraggable>
组件内部实现逻辑是默认直接将left
和top
属性定义在组件上了,如果不想让它生效就别给元素设置position: fixed || absolute
。
Component 使用方式暂还没想到要如何改造,我初步的设想是:
<script setup lang="ts">
import { UseDraggable } from '@vueuse/components'
</script>
<template>
<UseDraggable
v-slot="{ x, y }"
:style="{ transform: `translate(${x}px, ${y}px)` }"
>
<div>
{{ Math.round(x) }}, {{ Math.round(y) }}
</div>
</UseDraggable>
</template>
<style scoped></style>
但是style
中拿不到默认插槽上的参数,这种方式就放弃了,要是大佬有其他方式实现,欢迎大佬指点!
PS:主要需要注意的是transform
变换是基于元素本身的位置来计算的,这一点是特别需要注意的,需要根据自己的使用场景来判断是否需要使用transform
总结
下面是我总结的几个自己在此之前没有详细了解过知识点
- 变量解构:
const { handle: draggingHandle = target } = options
等效于const draggingHandle = options.handle || target
PointEvent
事件替代MouseEvent
:PointEvent
事件更通用,不仅包含鼠标,还包含触控笔和触摸屏- 渲染函数
h()
:创建虚拟 DOM 节点,并且在里面设置插槽 - 接受
props
的插槽:v-slot="{ x, y }"
解构默认插槽上的参数;v-slot:item="{ x, y }"
解构名为item
插槽上的参数
共同成长,无限进步!!! 💪
原文链接:https://juejin.cn/post/7226376235558699063 作者:土豆鸡蛋