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

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>

问题原因分析

  1. 重复创建Tooltip:updateTooltip函数中每次直接调用el.appendChild(tooltipDiv),未检查元素是否已存在,且tooltipDiv疑似全局变量,导致每次更新都会新增元素。
  2. 钩子使用问题:Vue3已移除update钩子,替代的updated时机晚于beforeUpdate,但核心问题是更新逻辑非幂等。
  3. 不必要的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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.22 14:57:21