普通视图

发现新文章,点击刷新页面。
昨天 — 2026年1月28日首页

vue3自定义指令合集-单例v-tooltip

作者 fubobo
2026年1月28日 18:17

背景

在实际项目中,Tooltip 往往会遇到这些问题:

  • 大多数使用组件形式实现(有时候项目中对于tooltip没有过多要求,使用UI组件的话,需要包一层组件,有点臃肿)
  • 每个元素一个 Tooltip,实例过多,性能差
  • 页面滚动 / 容器滚动后,Tooltip 位置不更新
  • Tooltip 抖动、闪烁、频繁销毁重建
  • 指令解绑不彻底,事件和监听器泄漏

目标

  • 全局只创建 一个 Tooltip 实例
  • 使用指令 v-tooltip 即插即用
  • 支持 top / right / bottom / left
  • 自动跟随目标元素位置变化
  • 无闪烁、无内存泄漏
  • TypeScript 类型安全

造个轮子,代码放在仓库了,各位大佬自取:gitee.com/Gitfubobo/v…


使用方式

<button v-tooltip="'删除后无法恢复'">删除</button>
<button v-tooltip.right="'更多操作'">操作</button>

image.png


整体设计思路

Tooltip 用单例统一管理,指令只负责绑定 DOM,这里的单例是指DOM单例,页面上只存在一个DOM

核心拆分

  • TooltipSingleton

    • 负责 Tooltip 的创建、更新、销毁
  import tooltipV from './tooltip.vue';
  
  
  /**
     * @Author: fubobo
     * @Description: 单例
     * @return {TooltipSingleton}
     */
    public static getInstance(): TooltipSingleton {
        if (!TooltipSingleton.instance) {
            TooltipSingleton.instance = new TooltipSingleton();
        }
        return TooltipSingleton.instance;
    }

    // 创建dom挂载tooltip组件
    private createInstance() {
        if (!this.tooltipInstance) {
            this.rootEl = document.createElement('div');
            document.body.appendChild(this.rootEl);
            this.app = createApp(tooltipV);
            this.tooltipInstance = this.app.mount(this.rootEl);
        }
    }
  • v-tooltip 指令

    • 负责 DOM 事件绑定与生命周期
 // 指令对象
 const tooltipDirective: Directive<HTMLElement, string> = {
    mounted(el, binding) {
        TooltipSingleton.getInstance().mount(el, binding);
    },
    updated(el, binding) {
        TooltipSingleton.getInstance().updated(el, binding);
    },
    unmounted(el) {
        TooltipSingleton.getInstance().unmount(el);
    },
};
  • 位置监听工具

    • 负责监听滚动 / resize / DOM 变化
  /**
     * @Author: fubobo
     * @Description: 根据目标元素和tooltip进行位置计算
     * @return 坐标
     * @param {HTMLElement} targetEl
     * @param {Placement} placement
     */
    private calculatePosition(targetEl: HTMLElement, placement: Placement) {
        ....
        return { x, y };
    }

/**
 * 监听元素距离窗口顶部的距离变化
 * @param element 目标元素
 * @param callback 距离变化回调
 * @param options 配置项
 * @returns 停止监听的函数
 */
const observeElementViewportTop = (
    element: HTMLElement,
    callback: PositionCallback,
    options?: {
        throttle?: number; // 节流时间(默认 100ms)
        observeAncestors?: boolean; // 是否监听祖先元素尺寸变化
    }
) => {
    ....
    return () => {
        window.removeEventListener('scroll', handleWindowEvent);
        window.removeEventListener('resize', handleWindowEvent);
        mutationObserver.disconnect();
        resizeObserver.disconnect();
    };
};

为什么一定要用「单例 Tooltip」

如果每个 v-tooltip 都创建一个组件:

  • DOM 数量指数级增长
  • 每个 Tooltip 都监听 scroll / resize
  • 事件难以统一管理

带来的好处

  • 整个应用 只存在一个 Tooltip
  • hover 不同元素 → 只是更新内容和位置
  • 性能稳定、结构清晰

Tooltip 实例的创建与销毁

创建(只会执行一次)

private createInstance() {
  this.rootEl = document.createElement('div');
  document.body.appendChild(this.rootEl);
  this.app = createApp(tooltipV);
  this.tooltipInstance = this.app.mount(this.rootEl);
}

销毁(最后一个指令卸载时)

private destroyInstance() {
  this.app.unmount();
  document.body.removeChild(this.rootEl);
}

Tooltip 定位计算逻辑

核心思路:

使用目标元素位置 + Tooltip 自身尺寸计算最终坐标

const tooltipRect = tooltip.getBoundingClientRect();
const targetRect = target.getBoundingClientRect();
switch (placement) {
  case 'top':
    x = targetRect.x + targetRect.width / 2 - tooltipRect.width / 2;
    y = targetRect.y - tooltipRect.height - 10;
}

监听「元素位置变化」

仅靠 mouseenter 是完全不够的:

  • 页面滚动
  • 容器滚动
  • DOM transform
  • 父级尺寸变化

解决方案:多维度监听

observeElementViewportTop(el, callback, {
  observeAncestors: true,
});

监听来源包括:

监听方式 解决问题
scroll / resize 页面和窗口变化
MutationObserver class / style 改变
ResizeObserver 元素或父级尺寸变化

📌 Tooltip 始终贴着目标元素,不会错位

关键点

  • Tooltip 自身维护 hover 状态
  • 延迟隐藏(100ms)
  • 体验更自然

使用 WeakMap 优化性能

  • 不阻止 GC
  • DOM 销毁后自动释放
  • 从根源避免内存泄漏
class TooltipSingleton {
    ...
    private eventHandlers = new WeakMap<
        HTMLElement,
        {
            enter: EventListener;
            leave: EventListener;
        }
    >(); // 存储元素的事件处理器(弱引用防止内存泄漏)
    private directiveInfo = new WeakMap<
        HTMLElement,
        {
            text: string;
            placement: Placement;
        }
    >(); // 存储元素关联的指令信息(弱引用)
}

解决了哪些问题

问题 是否解决
Tooltip 重复创建
滚动后位置错乱
hover 闪烁
内存泄漏
TypeScript 类型不安全
指令更新无效
支持DOM传值

代码仓库

框框造个轮子,代码放在仓库了,各位大佬自取:gitee.com/Gitfubobo/v…

❌
❌