在 Vue3 中,自定义指令(Custom Directives) 是对 DOM 元素进行底层操作的核心工具,它允许开发者封装可复用的 DOM 行为逻辑,弥补了 Vue 模板语法在“直接操作 DOM”场景下的不足。其核心作用是将重复的 DOM 操作抽象为指令,实现逻辑复用与代码解耦,同时保持模板的简洁性。

一、核心作用:解决“DOM 操作复用”问题

Vue 的核心思想是“数据驱动视图”,推荐通过修改数据间接控制 DOM,而非直接操作 DOM。但在某些场景下(如 DOM 元素聚焦、样式动态切换、事件监听绑定等),仍需直接操作 DOM,且这些操作可能在多个组件中重复出现。

自定义指令的核心价值就是将这些重复的 DOM 操作封装成指令,只需在模板中通过 v-xxx 调用,无需在每个组件中重复编写相同的 DOM 逻辑。

典型应用场景示例:
  1. 自动聚焦:页面加载后或某事件触发后,让输入框自动获取焦点(如登录页的用户名输入框)。
    若不使用自定义指令,需在每个组件中通过 ref 获取 DOM 并调用 focus(),逻辑重复;封装为 v-focus 后,只需在模板写 <input v-focus>

  2. 权限控制:根据用户权限决定是否显示某 DOM 元素(比 v-if 更底层,可直接移除 DOM 而非仅隐藏)。
    封装为 v-permission="['admin']",指令内部判断权限,无权限则直接删除元素。

  3. 防抖/节流:给按钮点击、输入框输入等事件添加防抖(避免频繁触发),封装为 v-debounce="handleClick",指令内部处理防抖逻辑。

  4. 动态样式/类名:复杂的样式切换逻辑(如根据滚动位置添加导航栏阴影),封装为 v-scroll-shadow,指令监听滚动事件并修改样式。

二、自定义指令的核心能力:生命周期钩子

Vue3 为自定义指令提供了 5 个生命周期钩子,开发者可在不同阶段操作 DOM,覆盖从“指令绑定”到“指令解绑”的全流程,确保逻辑的精细控制。

各钩子的作用与参数说明如下:

钩子函数 触发时机 核心参数(常用)
created 指令绑定到元素时(元素未挂载到 DOM) el(指令绑定的 DOM 元素)、binding(指令的参数/值,如 v-xxx="value" 中的 value
beforeMount 元素挂载到 DOM 前 created
mounted 元素挂载到 DOM 后(最常用,适合初始化操作) created,可安全操作 DOM(如调用 el.focus()
beforeUpdate 元素所在组件的 VNode 更新前 新增 prevVNode(更新前的 VNode),用于对比更新前后的状态
updated 元素所在组件的 VNode 及子 VNode 更新后 beforeUpdate
beforeUnmount 元素从 DOM 卸载前 created,适合清理操作(如移除事件监听)
unmounted 元素从 DOM 卸载后 created

三、自定义指令的优势

  1. 逻辑复用性:一次封装,多组件调用。例如 v-focus 可在登录页、搜索框、表单页等所有需要“自动聚焦”的场景复用,无需重复写 ref + focus() 逻辑。

  2. 模板简洁性:将复杂的 DOM 操作逻辑隐藏在指令内部,模板中仅需一个 v-xxx 指令,可读性更强。
    对比:

    • 无指令:<input ref="inputRef" @mounted="inputRef.focus()">(需在组件内定义 ref 和方法)
    • 有指令:<input v-focus>(模板极简,逻辑封装在指令中)
  3. 底层控制力:相比 computedwatch 等数据层面的 API,自定义指令直接操作 DOM,可实现更底层的功能(如修改元素属性、绑定原生事件、操作元素样式等)。

  4. 全局/局部灵活性:可注册为全局指令(全项目可用)或局部指令(仅当前组件可用),按需选择,避免全局污染。

四、Vue3 自定义指令的注册与使用示例

1. 局部指令(仅当前组件可用)

在组件的 directives 选项中定义,仅在当前组件的模板中生效:

<template>
  <!-- 使用局部指令 v-focus -->
  <input v-focus placeholder="自动聚焦输入框">
</template>

<script setup>
// 定义局部自定义指令 v-focus
const vFocus = {
  // 元素挂载到 DOM 后,执行聚焦操作
  mounted(el) {
    el.focus(); // 直接操作 DOM 元素
  }
};
</script>
2. 全局指令(全项目可用)

在入口文件(如 main.js)中通过 app.directive() 注册,所有组件均可使用:

// main.js
import { createApp } from 'vue';
import App from './App.vue';

const app = createApp(App);

// 注册全局指令 v-permission(权限控制)
app.directive('permission', {
  mounted(el, binding) {
    // binding.value 是指令的参数,如 v-permission="['admin']" 中的 ['admin']
    const userRoles = ['user']; // 假设当前用户角色是普通用户
    // 若用户角色不在允许的权限列表中,移除 DOM 元素
    if (!binding.value.some(role => userRoles.includes(role))) {
      el.parentNode.removeChild(el);
    }
  }
});

app.mount('#app');

在任意组件中使用:

<template>
  <!-- 仅 admin 角色可见的按钮 -->
  <button v-permission="['admin']">删除数据(仅管理员可见)</button>
</template>

五、注意事项

  1. 避免过度使用:自定义指令是“直接操作 DOM”的工具,若能通过 v-bindv-if、组件 props 等数据驱动方式实现,优先选择数据驱动(符合 Vue 设计思想)。

  2. 钩子参数的只读性binding 参数是只读的,不可修改;若需传递动态值,可通过 binding.value 接收组件中的响应式数据(值会随数据更新自动同步)。

  3. 服务器端渲染(SSR)兼容:在 SSR 场景下,mountedupdated 等钩子不会在服务端执行(因服务端无 DOM),需确保指令逻辑在 SSR 环境下不会报错(可通过 process.client 判断环境)。

总结

Vue3 自定义指令的核心作用是封装可复用的 DOM 操作逻辑,解决“多组件重复操作 DOM”的问题,同时保持模板简洁。它是 Vue 数据驱动思想的补充,适用于需直接控制 DOM 的场景(如聚焦、权限、防抖、样式动态控制等),是提升代码复用性与可维护性的重要工具。

自定义指令集合

import type { App, Directive, DirectiveBinding } from "vue";
import i18n from "@/locales"; //国际化

interface CopyBindingValue {
   text?: string;
   toast?: boolean;
}

const directivesMap: Record<string, Directive> = {
   copy: {
      mounted(el: HTMLElement, binding: DirectiveBinding<string | CopyBindingValue | undefined>) {
         const { t } = i18n.global;

         const handler = async () => {
            let text = "";
            let showToast = true;

            // ✅ 1️⃣ 处理绑定值
            if (typeof binding.value === "string") {
               text = binding.value;
            } else if (typeof binding.value === "object" && binding.value?.text) {
               text = binding.value.text;
               showToast = binding.value.toast !== false;
            }

            // ✅ 2️⃣ 如果没传 binding.value,取元素文字
            if (!text) {
               text = el.innerText?.trim() || el.textContent?.trim() || "";
            }

            // ✅ 3️⃣ 判空
            if (!text) {
               if (showToast) uni.showToast({ title: t("copy.empty"), icon: "none" });
               return;
            }

            // ✅ 4️⃣ 尝试复制
            try {
               await uni.setClipboardData({
                  data: text,
                  success() {
                     if (showToast) uni.showToast({ title: t("copy.success"), icon: "none", duration: 2000 });
                  },
                  fail() {
                     if (showToast) uni.showToast({ title: t("copy.fail"), icon: "none" });
                  },
               });
            } catch (err) {
               console.log("🚀 ~ handler ~ err:", err);
               if (showToast) uni.showToast({ title: t("copy.error"), icon: "none" });
            }
         };

         // ✅ 绑定事件
         el.addEventListener("click", handler);
         (el as any).__copyHandler__ = handler;
      },

      unmounted(el: HTMLElement) {
         const handler = (el as any).__copyHandler__;
         if (handler) el.removeEventListener("click", handler);
         delete (el as any).__copyHandler__;
      },
   },
};

export default function registerDirectives(app: App) {
   Object.keys(directivesMap).forEach((key) => {
      app.directive(key, directivesMap[key]);
   });
}

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐