VueUse探索2: useMouse 是如何实现的?

212 阅读5分钟

前言

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 } ,作用是:

  1. 性能优化:告诉浏览器该事件监听器不会调用 preventDefault(),允许浏览器立即执行滚动等默认行为,无需等待事件处理完成
  2. 避免警告:现代浏览器会对可能阻塞滚动的非 passive 事件监听器显示警告
  3. 触摸事件优化:特别适合 touchstarttouchmove 事件,可以显著提升移动端滚动性能

触摸事件处理

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 ,我们不需要手写代码去监听鼠标事件和触摸事件或是获取坐标值,显得更加高效、更加优雅。