vue3-自定义指令
在 Vue3 中,自定义指令(Custom Directives) 是对 DOM 元素进行底层操作的核心工具,它允许开发者封装可复用的 DOM 行为逻辑,弥补了 Vue 模板语法在“直接操作 DOM”场景下的不足。其核心作用是将重复的 DOM 操作抽象为指令,实现逻辑复用与代码解耦,同时保持模板的简洁性。
一、核心作用:解决“DOM 操作复用”问题
Vue 的核心思想是“数据驱动视图”,推荐通过修改数据间接控制 DOM,而非直接操作 DOM。但在某些场景下(如 DOM 元素聚焦、样式动态切换、事件监听绑定等),仍需直接操作 DOM,且这些操作可能在多个组件中重复出现。
自定义指令的核心价值就是将这些重复的 DOM 操作封装成指令,只需在模板中通过 v-xxx 调用,无需在每个组件中重复编写相同的 DOM 逻辑。
典型应用场景示例:
-
自动聚焦:页面加载后或某事件触发后,让输入框自动获取焦点(如登录页的用户名输入框)。
若不使用自定义指令,需在每个组件中通过ref获取 DOM 并调用focus(),逻辑重复;封装为v-focus后,只需在模板写<input v-focus>。 -
权限控制:根据用户权限决定是否显示某 DOM 元素(比
v-if更底层,可直接移除 DOM 而非仅隐藏)。
封装为v-permission="['admin']",指令内部判断权限,无权限则直接删除元素。 -
防抖/节流:给按钮点击、输入框输入等事件添加防抖(避免频繁触发),封装为
v-debounce="handleClick",指令内部处理防抖逻辑。 -
动态样式/类名:复杂的样式切换逻辑(如根据滚动位置添加导航栏阴影),封装为
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 |
三、自定义指令的优势
-
逻辑复用性:一次封装,多组件调用。例如
v-focus可在登录页、搜索框、表单页等所有需要“自动聚焦”的场景复用,无需重复写ref + focus()逻辑。 -
模板简洁性:将复杂的 DOM 操作逻辑隐藏在指令内部,模板中仅需一个
v-xxx指令,可读性更强。
对比:- 无指令:
<input ref="inputRef" @mounted="inputRef.focus()">(需在组件内定义ref和方法) - 有指令:
<input v-focus>(模板极简,逻辑封装在指令中)
- 无指令:
-
底层控制力:相比
computed、watch等数据层面的 API,自定义指令直接操作 DOM,可实现更底层的功能(如修改元素属性、绑定原生事件、操作元素样式等)。 -
全局/局部灵活性:可注册为全局指令(全项目可用)或局部指令(仅当前组件可用),按需选择,避免全局污染。
四、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>
五、注意事项
-
避免过度使用:自定义指令是“直接操作 DOM”的工具,若能通过
v-bind、v-if、组件 props 等数据驱动方式实现,优先选择数据驱动(符合 Vue 设计思想)。 -
钩子参数的只读性:
binding参数是只读的,不可修改;若需传递动态值,可通过binding.value接收组件中的响应式数据(值会随数据更新自动同步)。 -
服务器端渲染(SSR)兼容:在 SSR 场景下,
mounted、updated等钩子不会在服务端执行(因服务端无 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]);
});
}
更多推荐



所有评论(0)