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>
解决方案
核心问题分析
- 元素引用时机错误:组件初始化时
panel.value是null,DOM尚未挂载,此时传入Composable无法获取有效元素。 - 事件监听逻辑错误:
onMounted中错误使用removeEventListener,应该是addEventListener;且未处理元素引用变化的场景。 - 未追踪响应式变化:直接传入
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
相关产品推荐
相关产品推荐

