You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Vue 3组合式API:如何向Composable传入HTMLElement?

能否将HTMLElement传入Vue 3的Composable?如果可以,正确的实现方式是什么?

我想实现一个类似Vue官方鼠标位置追踪的Composable功能,但限制为仅在传入的元素内生效。目前遇到两个问题:

  • 在onMounted生命周期钩子前传入元素引用时,值为null;
  • 父组件调用onMounted时,Composable内的onMounted不会执行。

附上我的非工作代码示例:

mouse.ts

import { ref, watchEffect, onMounted, onUnmounted } from "vue"
import type { Ref } from "vue"

interface Coordinate {
    x: Ref<number>
    y: Ref<number>
}

export function useMouseIn(element: HTMLElement | null): Coordinate {
    const x = ref(0)
    const y = ref(0)

    if (!element) {
        return { x , y }
    }

    function update(event: MouseEvent) {
        if (!element) {
            return
        }

        const rect = element.getBoundingClientRect()
        x.value = Math.ceil(event.clientX - rect.left)
        y.value = Math.ceil(event.clientY - rect.top)

        x.value = x.value < 0 ? 0 : x.value
        y.value = y.value < 0 ? 0 : y.value
    }

    onMounted(() => element.removeEventListener("mousemove", update))
    onUnmounted(() => element.removeEventListener("mousemove", update))

    return { x, y }
}

Panel.vue

<script setup lang="ts">
    import { ref } from "vue"
    import { useMouseIn } from "../composables/mouse"

    const panel = ref<HTMLElement | null>(null)
    const { x, y } = useMouseIn(panel.value)
</script>

<template>
    <div
        ref="panel"
        class="panel"
    >
    {{ x }}, {{ y }}
    </div>
</template>

<style scoped>
    .panel {
        width: 200px;
        height: 200px;
        background-color: grey;
    }
</style>

App.vue

<script setup lang="ts">
    import Panel from "./components/Panel.vue"
</script>

<template>
    <div>
        <Panel />
    </div>
</template>

解决方案

核心问题分析

  1. 元素引用时机错误:组件初始化时panel.value是null,DOM尚未挂载,此时传入Composable无法获取有效元素。
  2. 事件监听逻辑错误:onMounted中错误使用removeEventListener,应该是addEventListener;且未处理元素引用变化的场景。
  3. 未追踪响应式变化:直接传入panel.value是静态值,无法感知元素挂载后的更新。

正确实现方式

修改useMouseIn,让它接收响应式元素引用,并用watchEffect自动管理事件监听的添加与移除:

优化后的mouse.ts

import { ref, watchEffect } from "vue"
import type { Ref } from "vue"

interface Coordinate {
    x: Ref<number>
    y: Ref<number>
}

// 接收响应式元素引用
export function useMouseIn(elementRef: Ref<HTMLElement | null>): Coordinate {
    const x = ref(0)
    const y = ref(0)

    const update = (event: MouseEvent) => {
        const element = elementRef.value
        if (!element) return

        const rect = element.getBoundingClientRect()
        x.value = Math.max(0, Math.ceil(event.clientX - rect.left))
        y.value = Math.max(0, Math.ceil(event.clientY - rect.top))
    }

    // watchEffect自动处理监听的生命周期
    watchEffect((onCleanup) => {
        const element = elementRef.value
        if (!element) return

        element.addEventListener("mousemove", update)
        // 清理函数:元素变化或组件卸载时移除监听
        onCleanup(() => {
            element.removeEventListener("mousemove", update)
        })
    })

    return { x, y }
}

修改后的Panel.vue

<script setup lang="ts">
    import { ref } from "vue"
    import { useMouseIn } from "../composables/mouse"

    const panel = ref<HTMLElement | null>(null)
    // 直接传入响应式引用,而非.value
    const { x, y } = useMouseIn(panel)
</script>

<template>
    <div
        ref="panel"
        class="panel"
    >
    {{ x }}, {{ y }}
    </div>
</template>

<style scoped>
    .panel {
        width: 200px;
        height: 200px;
        background-color: grey;
    }
</style>

关键改进点

  • 接收响应式引用:让Composable感知元素的挂载状态变化,而非一次性传入静态值。
  • 使用watchEffect:自动处理元素出现/消失时的事件监听添加与移除,无需手动依赖onMounted/onUnmounted,逻辑更简洁可靠。
  • 简化坐标计算:用Math.max替代三元表达式,代码更简洁。
  • 修复事件监听逻辑:纠正addEventListener的使用,并通过清理函数确保监听被正确移除。

参考库推荐

如果不想手动实现,可使用成熟的Vue工具库:

  • @vueuse/core:其中的useMouseInElement是现成实现,完全满足需求,还支持更多配置项。

内容的提问来源于stack exchange,提问作者Daniel Tate

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.02 15:30:22