前言
VueUse 是什么?根据官网的说法,VueUse 是依据 Vue 组合 API 的工具函数的集合。使用 VueUse,可以让我们编写代码更加地高效和快捷。
之前我们探索了最简单的 useCounter(计数器)例子的用法及其实现原理,本文会进一步探索 useMouse 的用法和实现原理。
基础用法
从官方文档可以看到,基础用法如下:
<script setup>
import { useMouse } from '@vueuse/core'
const { x, y, sourceType } = useMouse()
</script>
<div>
position: {{x}} {{y}}
{{sourceType}}
</div>
引入@vueuse/core后,我们就可以通过useMouse()的方式引入坐标变量 x, y 和 sourceType。当移动鼠标时,对应的坐标值会实时地更新到变量上。
可以去 playground 在线测试下。
实现原理
源码
// packages\core\useMouse\index.ts
import type { ConfigurableEventFilter } from '@vueuse/shared'
import type { MaybeRefOrGetter } from 'vue'
import type { ConfigurableWindow } from '../_configurable'
import type { Position } from '../types'
import { shallowRef } from 'vue'
import { defaultWindow } from '../_configurable'
import { useEventListener } from '../useEventListener'
export type UseMouseCoordType = 'page' | 'client' | 'screen' | 'movement'
export type UseMouseSourceType = 'mouse' | 'touch' | null
export type UseMouseEventExtractor = (event: MouseEvent | Touch) => [x: number, y: number] | null | undefined
export interface UseMouseOptions extends ConfigurableWindow, ConfigurableEventFilter {
/**
* Mouse position based by page, client, screen, or relative to previous position
*
* @default 'page'
*/
type?: UseMouseCoordType | UseMouseEventExtractor
/**
* Listen events on `target` element
*
* @default 'Window'
*/
target?: MaybeRefOrGetter<Window | EventTarget | null | undefined>
/**
* Listen to `touchmove` events
*
* @default true
*/
touch?: boolean
/**
* Listen to `scroll` events on window, only effective on type `page`
*
* @default true
*/
scroll?: boolean
/**
* Reset to initial value when `touchend` event fired
*
* @default false
*/
resetOnTouchEnds?: boolean
/**
* Initial values
*/
initialValue?: Position
}
const UseMouseBuiltinExtractors: Record<UseMouseCoordType, UseMouseEventExtractor> = {
page: event => [event.pageX, event.pageY],
client: event => [event.clientX, event.clientY],
screen: event => [event.screenX, event.screenY],
movement: event => (event instanceof MouseEvent
? [event.movementX, event.movementY]
: null
),
} as const
/**
* Reactive mouse position.
*
* @see https://vueuse.org/useMouse
* @param options
*/
export function useMouse(options: UseMouseOptions = {}) {
const {
type = 'page',
touch = true,
resetOnTouchEnds = false,
initialValue = { x: 0, y: 0 },
window = defaultWindow,
target = window,
scroll = true,
eventFilter,
} = options
let _prevMouseEvent: MouseEvent | null = null
let _prevScrollX = 0
let _prevScrollY = 0
const x = shallowRef(initialValue.x)
const y = shallowRef(initialValue.y)
const sourceType = shallowRef<UseMouseSourceType>(null)
const extractor = typeof type === 'function'
? type
: UseMouseBuiltinExtractors[type]
const mouseHandler = (event: MouseEvent) => {
const result = extractor(event)
_prevMouseEvent = event
if (result) {
[x.value, y.value] = result
sourceType.value = 'mouse'
}
if (window) {
_prevScrollX = window.scrollX
_prevScrollY = window.scrollY
}
}
const touchHandler = (event: TouchEvent) => {
if (event.touches.length > 0) {
const result = extractor(event.touches[0])
if (result) {
[x.value, y.value] = result
sourceType.value = 'touch'
}
}
}
const scrollHandler = () => {
if (!_prevMouseEvent || !window)
return
const pos = extractor(_prevMouseEvent)
if (_prevMouseEvent instanceof MouseEvent && pos) {
x.value = pos[0] + window.scrollX - _prevScrollX
y.value = pos[1] + window.scrollY - _prevScrollY
}
}
const reset = () => {
x.value = initialValue.x
y.value = initialValue.y
}
const mouseHandlerWrapper = eventFilter
? (event: MouseEvent) => eventFilter(() => mouseHandler(event), {} as any)
: (event: MouseEvent) => mouseHandler(event)
const touchHandlerWrapper = eventFilter
? (event: TouchEvent) => eventFilter(() => touchHandler(event), {} as any)
: (event: TouchEvent) => touchHandler(event)
const scrollHandlerWrapper = eventFilter
? () => eventFilter(() => scrollHandler(), {} as any)
: () => scrollHandler()
if (target) {
const listenerOptions = { passive: true }
useEventListener(target, ['mousemove', 'dragover'], mouseHandlerWrapper, listenerOptions)
if (touch && type !== 'movement') {
useEventListener(target, ['touchstart', 'touchmove'], touchHandlerWrapper, listenerOptions)
if (resetOnTouchEnds)
useEventListener(target, 'touchend', reset, listenerOptions)
}
if (scroll && type === 'page')
useEventListener(window, 'scroll', scrollHandlerWrapper, listenerOptions)
}
return {
x,
y,
sourceType,
}
}
export type UseMouseReturn = ReturnType<typeof useMouse>
首先,我们可以看到,useMouse函数的参数是 options,返回值是对象 { x, y, sourceType }
在函数内部:
const x = shallowRef(initialValue.x)
const y = shallowRef(initialValue.y)
const sourceType = shallowRef<UseMouseSourceType>(null)
- x 和 y 都是 shallowRef() 定义的,也就是创建的是浅层响应式的引用(Ref),而初始值指定了 initialValue 的值为 { x: 0, y: 0 }。
- sourceType 的值初始设置为 null。
target 的值是 window,我们可以看到:
if (target) {
const listenerOptions = { passive: true }
useEventListener(target, ['mousemove', 'dragover'], mouseHandlerWrapper, listenerOptions)
if (touch && type !== 'movement') {
useEventListener(target, ['touchstart', 'touchmove'], touchHandlerWrapper, listenerOptions)
if (resetOnTouchEnds)
useEventListener(target, 'touchend', reset, listenerOptions)
}
if (scroll && type === 'page')
useEventListener(window, 'scroll', scrollHandlerWrapper, listenerOptions)
}
这段代码主要包括以下三点:调用 useEventListener() 、触摸事件处理和滚动事件处理。
useEventListener
使用 useEventListener 去监听 target 对象的 mousemove 和 dragover 事件,使用包装后的 mouseHandlerWrapper 处理事件。通过 useEventListener 自动管理事件生命周期。实际上是调用了:
el.addEventListener(event, listener, options)
addEventListener 第三个参数 { passive: true } ,作用是:
- 性能优化:告诉浏览器该事件监听器不会调用
preventDefault(),允许浏览器立即执行滚动等默认行为,无需等待事件处理完成 - 避免警告:现代浏览器会对可能阻塞滚动的非 passive 事件监听器显示警告
- 触摸事件优化:特别适合
touchstart和touchmove事件,可以显著提升移动端滚动性能
触摸事件处理
if (touch && type !== 'movement') {
useEventListener(target, ['touchstart', 'touchmove'], touchHandlerWrapper, listenerOptions)
if (resetOnTouchEnds)
useEventListener(target, 'touchend', reset, listenerOptions)
}
当启用触摸时(touch=true),监听触摸事件
movement 类型不处理触摸事件(因移动量计算仅适用于鼠标)
可选配置 resetOnTouchEnds 在触摸结束时重置位置
滚动事件处理
if (scroll && type === 'page')
useEventListener(window, 'scroll', scrollHandlerWrapper, listenerOptions)
仅当启用滚动且坐标类型为 page 时生效
通过 scrollHandlerWrapper 处理页面滚动时的位置更新
mouseHandler 函数
接下来再看 mouseHandler 函数:
const mouseHandler = (event: MouseEvent) => {
// 1. 使用提取器获取坐标值
const result = extractor(event)
// 2. 保存当前鼠标事件用于后续处理
_prevMouseEvent = event
// 3. 如果有有效结果,更新坐标值x、y,更新事件来源为鼠标
if (result) {
[x.value, y.value] = result
sourceType.value = 'mouse'
}
// 4. 记录当前滚动位置(用于page类型坐标计算)
if (window) {
_prevScrollX = window.scrollX
_prevScrollY = window.scrollY
}
}
这段代码是鼠标事件处理的核心逻辑,主要功能是处理鼠标移动事件并更新响应式坐标值,与前面的 extractor 配置和后面的 scrollHandler 共同构成了完整的坐标跟踪系统。
(1)坐标提取器:extractor 根据配置的 type 从事件对象中提取坐标。
再看 extractor 函数:
const extractor = typeof type === 'function'
? type
: UseMouseBuiltinExtractors[type]
判断 type 类型:
-
如果 type 是函数(即用户自定义的提取函数) → 直接作为提取器使用
-
如果 type 是字符串 → 从内置提取器对象(page/client/screen/movement)中获取对应方法
其中,内置提取器:
const UseMouseBuiltinExtractors: Record<UseMouseCoordType, UseMouseEventExtractor> = {
page: event => [event.pageX, event.pageY], // 页面坐标
client: event => [event.clientX, event.clientY], // 视口坐标
screen: event => [event.screenX, event.screenY], // 屏幕坐标
movement: event => (event instanceof MouseEvent // 移动增量
? [event.movementX, event.movementY]
: null
),
} as const
(2)滚动感知:
- 专门记录滚动位置,确保
page类型坐标在滚动时能正确计算 - 为后续的
scrollHandler提供基准数据
类似地,touchHandler 函数和 scrollHandler 函数就不再赘述了。
后记
总而言之,使用 useMouse ,我们不需要手写代码去监听鼠标事件和触摸事件或是获取坐标值,显得更加高效、更加优雅。