Vue3自定义Tooltip指令重复创建元素问题求助
Vue3自定义Tooltip指令问题解决
问题描述
我开发了一个Vue3自定义Tooltip指令,该指令仅在beforeUpdate钩子中能正常工作,使用update钩子时无法正常运行。当前遇到的问题是:当鼠标悬停在绑定了mouseover和mouseleave事件的h1元素上时,指令会重复创建Tooltip元素。作为Vue自定义指令的初学者,希望能获得解决该问题的帮助。
现有代码
HTML结构
<div class="tooltip"><h1>Hello world from html</h1></div> <div class="tooltip"><h1>Hello world from html</h1></div> <div class="tooltip"><h1>Hello world from html</h1></div> <div class="tooltip"><h1>Hello world from html</h1></div>
tooltip.ts
const updateTooltip = (el: HTMLElement, value: DirectiveBinding["value"], modifiers: DirectiveBinding["modifiers"], arg: DirectiveBinding["arg"], vnode: VNode): void => { if (!value.text) { el.classList.remove("tooltip-parent") } if (value.text) { tooltipDiv.classList.add("tooltip") tooltipDiv.innerHTML = value.text el.appendChild(tooltipDiv) } if (value.displayArrow) { el.style.setProperty(Tooltip.arrowDisplay, "inline") argTooltip?.style.setProperty(Tooltip.arrowDisplay, "inline") } tooltipPlacement(value, el, argTooltip, argTooltipDiv) } export const tooltip = { mounted: (el: HTMLElement, { value, modifiers, arg }: DirectiveBinding): void => { if (typeof value === "object" && value.text) { el.classList.add("tooltip-parent") } if (typeof value === "string" && value) { el.classList.add("tooltip-parent") } updateTooltip(el, value, modifiers, arg) }, beforeUpdate: (el: HTMLElement, { value, modifiers, arg }: DirectiveBinding): void => { updateTooltip(el, value, modifiers, arg) }, }
tooltipPlacement.ts
export const tooltipPlacement = (value: DirectiveBinding["value"], el: HTMLElement, argTooltip: HTMLElement, argTooltipDiv: HTMLDivElement) => { if (value.theme) { for (const [key, val] of Object.entries(value.theme) as [string, any][]) { switch (key) { case "hide": watch( () => val, () => { if (val) { argTooltip?.style.setProperty(Tooltip.opacity, "0") argTooltip?.style.setProperty(Tooltip.visibility, "hidden") /**** arrow ****/ argTooltip?.style.setProperty(Tooltip.arrowOpacity, "0") argTooltip?.style.setProperty(Tooltip.arrowVisibility, "hidden") } if (!val) { console.log("show") argTooltip?.style.setProperty(Tooltip.opacity, "1") argTooltip?.style.setProperty(Tooltip.visibility, "visible") /**** arrow ****/ argTooltip?.style.setProperty(Tooltip.arrowOpacity, "1") argTooltip?.style.setProperty(Tooltip.arrowVisibility, "visible") } }, { immediate: true }, ) break default: break } } } }
组件Template
<template> <div> <div class="p-6 centered flex-col space-y-6"> {{ !!hideTooltip }} <h1>Tooltip</h1> <br /> <p v-tooltip:actions.arrow="tooltipContent(html, tooltipTheme, true)" class="border w-40 h-20 text-center">hello world</p> </div> <div class="p-6 centered flex-col space-y-6"> <h1 class="h-20" @mouseover="hideTooltip = false" @mouseleave="hideTooltip = true">Actions</h1> <br /> <p class="h-60 border px-4 actions">Test of args tooltip</p> <button class="mt-10 p-6" @click="hideTooltip = !hideTooltip">{{ hideTooltip ? "Show Tooltip" : "Hide" }}</button> </div> </div> </template>
组件Script
<script lang="ts" setup> import { tooltipContent, ThemeOptions } from "../directives/tooltip" import { ref, computed } from "vue" const hideTooltip = ref(true) const html = ref("<h1>Hello world from html</h1>") const tooltipTheme = computed((): ThemeOptions => { return { placement: "leftTop", argTxt: html.value, hide: !!hideTooltip.value, maxWidth: "300px", } }) </script>
问题原因分析
- 重复创建Tooltip:
updateTooltip函数中每次直接调用el.appendChild(tooltipDiv),未检查元素是否已存在,且tooltipDiv疑似全局变量,导致每次更新都会新增元素。 - 钩子使用问题:Vue3已移除
update钩子,替代的updated时机晚于beforeUpdate,但核心问题是更新逻辑非幂等。 - 不必要的watch:在
tooltipPlacement中每次钩子触发都创建新的watch,导致重复监听,且指令钩子本身会在值变化时触发,无需额外watch。
解决方案
1. 修复重复创建问题
修改updateTooltip函数,优先复用已存在的Tooltip元素,避免重复添加:
const updateTooltip = (el: HTMLElement, value: DirectiveBinding["value"], modifiers: DirectiveBinding["modifiers"], arg: DirectiveBinding["arg"]): void => { // 移除已存在的tooltip(当不需要时) const existingTooltip = el.querySelector('[data-tooltip]') as HTMLDivElement; if (!value.text) { el.classList.remove("tooltip-parent"); existingTooltip?.remove(); return; } el.classList.add("tooltip-parent"); // 复用或创建tooltip元素 let tooltipDiv = existingTooltip; if (!tooltipDiv) { tooltipDiv = document.createElement('div'); tooltipDiv.setAttribute('data-tooltip', ''); tooltipDiv.classList.add("tooltip"); el.appendChild(tooltipDiv); } // 更新内容 tooltipDiv.innerHTML = value.text; if (value.displayArrow) { el.style.setProperty(Tooltip.arrowDisplay, "inline"); const argTooltip = el.querySelector('.tooltip-arg'); // 根据实际结构调整选择器 argTooltip?.style.setProperty(Tooltip.arrowDisplay, "inline"); } tooltipPlacement(value, el, argTooltip, tooltipDiv); }
2. 移除不必要的watch,直接处理样式
修改tooltipPlacement,去掉watch,直接根据当前值设置样式:
export const tooltipPlacement = (value: DirectiveBinding["value"], el: HTMLElement, argTooltip: HTMLElement) => { if (value.theme) { const { hide } = value.theme; if (typeof hide !== 'undefined') { const isHidden = !!hide; argTooltip?.style.setProperty(Tooltip.opacity, isHidden ? "0" : "1"); argTooltip?.style.setProperty(Tooltip.visibility, isHidden ? "hidden" : "visible"); argTooltip?.style.setProperty(Tooltip.arrowOpacity, isHidden ? "0" : "1"); argTooltip?.style.setProperty(Tooltip.arrowVisibility, isHidden ? "hidden" : "visible"); } // 处理其他theme属性... } }
3. 优化指令钩子,添加清理逻辑
完善指令的unmounted钩子,避免内存泄漏:
export const tooltip = { mounted: (el: HTMLElement, { value, modifiers, arg }: DirectiveBinding): void => { if ((typeof value === "object" && value.text) || (typeof value === "string" && value)) { el.classList.add("tooltip-parent"); } updateTooltip(el, value, modifiers, arg); }, beforeUpdate: (el: HTMLElement, { value, modifiers, arg }: DirectiveBinding): void => { updateTooltip(el, value, modifiers, arg); }, unmounted: (el: HTMLElement) => { const existingTooltip = el.querySelector('[data-tooltip]'); if (existingTooltip) { existingTooltip.remove(); } el.classList.remove("tooltip-parent"); } }
4. 关于钩子的说明
Vue3中update钩子已被废弃,若需在组件更新后处理逻辑,可使用updated钩子,但优先保证beforeUpdate中的逻辑是幂等的(多次执行结果一致),这样就能避免重复操作问题。
内容的提问来源于stack exchange,提问作者Jorge Alberto Alvarez Sone
相关产品推荐
相关产品推荐

