Vue 除了提供 v-model、v-show、v-bind 等内置指令外,还允许开发者注册自定义指令(Custom Directives),用于封装涉及普通元素的底层 DOM 访问逻辑,弥补内置指令的灵活性不足。自定义指令的核心作用是复用 DOM 相关的重复操作,无需在组件的生命周期钩子中编写大量冗余代码,尤其适合焦点控制、权限控制、输入校验、动画效果等场景,是 Vue 开发中提升代码复用性和可维护性的重要手段。
本文将详细讲解 Vue 自定义指令的核心概念、注册方式、钩子函数、参数说明,结合 Vue2 与 Vue3 的语法差异,提供可直接复制的实战示例和进阶用法,兼顾新手入门与企业级实战需求。
一、自定义指令核心基础(必懂)
1. 核心定位
自定义指令主要用于处理底层 DOM 操作 ,与组件、组合式函数形成互补:组件是主要的构建模块,组合式函数侧重于有状态的逻辑,而自定义指令则专注于 DOM 元素的直接操作。需要注意的是,若功能可通过 v-bind 等内置指令或组件实现,优先选择内置指令,因其更高效、对服务端渲染更友好。
2. 命名规范
自定义指令的命名需遵循以下规范,确保兼容性和可读性:
- 指令名不包含
v-前缀(注册时无需写,使用时必须加v-); - 命名采用"小写字母 + 连字符"形式(如
v-focus、v-permission),避免驼峰式(Vue3 中虽支持驼峰命名,但模板中仍需转为连字符形式); - 避免与 Vue 内置指令重名(如不能命名为
v-model、v-show)。
3. 核心分类
根据作用域,自定义指令分为两类,适配不同使用场景:
- 全局指令:在整个 Vue 应用中注册,所有组件均可直接使用,适合通用型场景(如
v-focus、v-loading); - 局部指令:仅在单个组件内注册,仅当前组件可用,适合组件专属的 DOM 操作场景。
二、自定义指令的注册方式(Vue2+Vue3对比)
Vue2 与 Vue3 的注册方式核心差异在于"全局注册的调用对象",局部注册逻辑基本一致,以下是完整实战示例。
1. 全局注册(推荐通用指令使用)
javascript
// 1. Vue2 全局注册(main.js)
import Vue from 'vue'
import App from './App.vue'
// 全局注册 v-focus 指令(实现输入框自动聚焦)
Vue.directive('focus', {
// 钩子函数(后续详解)
mounted(el) {
el.focus() // 直接操作DOM元素
}
})
new Vue({
render: h => h(App)
}).$mount('#app')
// 2. Vue3 全局注册(main.js)
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
// 全局注册 v-focus 指令,语法与Vue2一致,仅注册对象不同
app.directive('focus', {
mounted(el) {
el.focus()
}
})
app.mount('#app')
2. 局部注册(推荐组件专属指令使用)
xml
// 1. Vue2 局部注册(组件内)
<template>
<input v-focus type="text" placeholder="自动聚焦输入框" />
</template>
<script>
export default {
// 局部注册指令,仅当前组件可用
directives: {
focus: {
mounted(el) {
el.focus()
}
}
}
}
</script>
// 2. Vue3 局部注册(选项式API,与Vue2一致)
<template>
<input v-focus type="text" placeholder="自动聚焦输入框" />
</template>
<script>
export default {
directives: {
focus: {
mounted(el) {
el.focus()
}
}
}
}
</script>
// 3. Vue3 局部注册(组合式API,<script setup>)
<template>
<input v-focus type="text" placeholder="自动聚焦输入框" />
</template>
<script setup>
// 组合式API中,直接定义以v开头的驼峰变量,即可完成局部注册
// 变量名vFocus,模板中使用时需转为v-focus
const vFocus = {
mounted(el) {
el.focus()
}
}
</script>
说明:Vue3 组合式 API 中,无需在 directives 选项中注册,只要定义以 v 开头的驼峰式变量(如 vFocus),即可在模板中以 v-focus 形式使用,简化了局部注册流程。
三、自定义指令的钩子函数(核心)
自定义指令的本质是一组钩子函数的集合,用于在指令生命周期的不同阶段执行 DOM 操作。Vue2 与 Vue3 的钩子函数名称和执行时机有差异,核心逻辑一致,以下分版本详解。
1. Vue3 钩子函数(7个,推荐)
Vue3 提供 7 个钩子函数,覆盖指令从绑定到卸载的完整生命周期,按执行顺序排列如下:
javascript
app.directive('custom', {
// 1. created:指令绑定到元素后立即调用(元素未插入DOM,无法操作DOM)
created(el, binding, vnode) {},
// 2. beforeMount:元素被插入DOM前调用
beforeMount(el, binding, vnode) {},
// 3. mounted:元素被插入DOM后调用(最常用,适合执行初始化DOM操作)
mounted(el, binding, vnode) {},
// 4. beforeUpdate:包含指令的组件更新前调用(子组件未更新)
beforeUpdate(el, binding, vnode, prevVnode) {},
// 5. updated:包含指令的组件更新后调用(子组件已更新,适合更新DOM状态)
updated(el, binding, vnode, prevVnode) {},
// 6. beforeUnmount:元素被卸载前调用(可做清理前准备)
beforeUnmount(el, binding, vnode) {},
// 7. unmounted:元素被卸载后调用(必须清理资源,避免内存泄漏)
unmounted(el, binding, vnode) {}
})
2. Vue2 钩子函数(5个)
Vue2 的钩子函数与 Vue3 对应,名称和执行时机略有差异,核心功能一致,按执行顺序排列如下:
javascript
Vue.directive('custom', {
// 1. bind:指令第一次绑定到元素时调用(仅一次,元素未插入DOM,可做初始化设置)
bind(el, binding, vnode) {},
// 2. inserted:元素被插入父节点时调用(仅保证父节点存在,不一定插入文档)
inserted(el, binding, vnode) {},
// 3. update:包含指令的组件VNode更新时调用(子组件可能未更新)
update(el, binding, vnode, oldVnode) {},
// 4. componentUpdated:组件VNode及其子VNode全部更新后调用
componentUpdated(el, binding, vnode, oldVnode) {},
// 5. unbind:指令与元素解绑时调用(仅一次,用于清理资源)
unbind(el, binding, vnode) {}
})
3. 钩子函数参数(Vue2/Vue3通用)
所有钩子函数都会接收 4 个固定参数(顺序不可变),除 el 外,其他参数均为只读,不可修改,若需共享数据,可通过 el 的自定义属性实现:
-
el:指令绑定的真实 DOM 元素,可直接操作(如el.focus()、el.style.color = 'red'); -
binding:指令绑定信息的对象,核心属性如下:value:传递给指令的值(如v-custom="100",value 为 100);oldValue:指令的旧绑定值,仅在update/componentUpdated(Vue2)、beforeUpdate/updated(Vue3)中可用;arg:指令的参数(如v-custom:click,arg 为 'click');modifiers:指令的修饰符对象(如v-custom.prevent,modifiers 为 { prevent: true });name:指令名(不包含v-前缀)。
-
vnode:Vue 编译生成的虚拟节点,描述 DOM 元素的结构; -
prevVnode(Vue3)/oldVnode(Vue2):上一个虚拟节点,仅在更新相关钩子中可用。
4. 钩子函数简化写法
若仅需使用 mounted(Vue3)或 bind + inserted(Vue2)一个钩子函数,可简化为函数形式,无需定义完整的钩子对象:
javascript
// Vue3 简化写法(仅使用mounted钩子)
app.directive('focus', (el) => {
el.focus() // 等同于 { mounted: (el) => el.focus() }
})
// Vue2 简化写法(等同于 bind + inserted 钩子执行相同逻辑)
Vue.directive('focus', (el) => {
el.focus()
})
四、Vue2与Vue3自定义指令核心差异汇总
为方便快速区分和项目迁移,整理核心差异如下,重点关注钩子函数和注册方式的差异:
| 对比维度 | Vue2 | Vue3 |
|---|---|---|
| 全局注册方式 | Vue.directive('指令名', 钩子对象/函数) | app.directive('指令名', 钩子对象/函数) |
| 局部注册方式 | 仅支持 directives 选项注册 | 支持 directives 选项 + |
| 钩子函数 | bind、inserted、update、componentUpdated、unbind(5个) | created、beforeMount、mounted、beforeUpdate、updated、beforeUnmount、unmounted(7个) |
| 核心差异点 | 无 created、beforeMount、beforeUnmount 钩子 | 新增3个钩子,完善生命周期覆盖;支持组合式API集成 |
| 虚拟节点参数 | update/componentUpdated 接收 oldVnode | beforeUpdate/updated 接收 prevVnode |
五、实战示例(可直接复制使用)
以下示例覆盖企业级开发中高频场景,适配 Vue2 和 Vue3,标注清晰,复制后可直接集成到项目中。
示例1:v-focus(自动聚焦,基础示例)
实现输入框挂载后自动聚焦,比原生 autofocus 属性更实用,可在 Vue 动态插入元素时生效。
javascript
// Vue3 实现(全局注册,main.js)
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
// 自动聚焦指令
app.directive('focus', {
mounted(el) {
el.focus() // 元素挂载后执行聚焦
}
})
app.mount('#app')
// 模板中使用(所有组件均可使用)
<template>
<input v-focus type="text" placeholder="自动聚焦输入框" />
</template>
// Vue2 实现(全局注册,main.js)
import Vue from 'vue'
import App from './App.vue'
Vue.directive('focus', {
inserted(el) {
el.focus() // Vue2 用inserted钩子,确保元素已插入DOM
}
})
new Vue({
render: h => h(App)
}).$mount('#app')
示例2:v-permission(权限控制,后台系统必用)
根据用户权限控制元素显隐,无对应权限则移除元素,适用于按钮、菜单等权限管控场景。
javascript
// Vue3 实现(全局注册,directives/permission.js)
import { useUserStore } from '@/stores/user' // 假设使用Pinia管理用户状态
export default {
mounted(el, binding) {
const userStore = useUserStore()
const permission = binding.value // 接收权限码(如 'user:add')
if (!permission) return
// 无权限则移除元素
if (!userStore.permissions.includes(permission)) {
el.parentNode?.removeChild(el)
}
}
}
// main.js 引入注册
import permission from './directives/permission'
app.directive('permission', permission)
// 模板中使用
<button v-permission="'user:add'">添加用户</button>
<button v-permission="'user:delete'">删除用户</button>
// Vue2 实现(全局注册)
import Vue from 'vue'
import store from './store' // Vuex管理用户状态
Vue.directive('permission', {
inserted(el, binding) {
const permission = binding.value
if (!permission) return
if (!store.state.user.permissions.includes(permission)) {
el.parentNode?.removeChild(el)
}
}
})
示例3:v-debounce(防抖点击,防重复提交)
实现按钮点击防抖,避免用户快速点击导致重复请求,适用于搜索、提交等场景。
xml
// Vue3 实现(局部注册,组件内)
<script setup>
// 防抖指令
const vDebounce = {
mounted(el, binding) {
const { func, delay = 300 } = binding.value // 接收函数和延迟时间
let timer = null
// 绑定点击事件,实现防抖
el.addEventListener('click', () => {
clearTimeout(timer)
timer = setTimeout(() => func(), delay)
})
// 卸载时清理定时器,避免内存泄漏
el._timer = timer
},
unmounted(el) {
clearTimeout(el._timer)
}
}
// 点击事件
const handleSubmit = () => {
console.log('提交表单')
}
</script>
<template>
<button v-debounce="{ func: handleSubmit, delay: 500 }">提交</button>
</template>
示例4:v-lazy(图片懒加载,性能优化)
实现图片懒加载,当图片进入视口后再加载,减少首屏资源请求,提升加载速度。
javascript
// Vue3 实现(全局注册)
app.directive('lazy', {
mounted(el, binding) {
// 监听元素是否进入视口
const observer = new IntersectionObserver(([{ isIntersecting }]) => {
if (isIntersecting) {
el.src = binding.value // 进入视口后加载图片
observer.unobserve(el) // 加载完成后停止监听
}
})
observer.observe(el) // 开始监听元素
}
})
// 模板中使用(src绑定占位图,v-lazy绑定真实图片地址)
<template>
<img v-lazy="realImgUrl" src="placeholder.png" alt="懒加载图片" />
</template>
六、自定义指令进阶用法
1. 指令传递动态参数和修饰符
通过 arg 传递动态参数,modifiers 传递修饰符,实现更灵活的指令逻辑:
xml
<template>
<!-- 动态参数:click(触发事件),修饰符:prevent(阻止默认行为) -->
<button v-custom:click.prevent="handleClick">点击触发</button>
</template>
<script setup>
const vCustom = {
mounted(el, binding) {
const event = binding.arg // 接收动态参数:click
const { prevent } = binding.modifiers // 接收修饰符:prevent
// 绑定事件
el.addEventListener(event, (e) => {
// 若有prevent修饰符,阻止默认行为
if (prevent) e.preventDefault()
binding.value() // 执行传递的函数
})
}
}
const handleClick = () => {
console.log('点击事件触发')
}
</script>
2. 指令与组件实例交互
Vue3 中,可通过 binding.instance 访问使用指令的组件实例,实现指令与组件的联动:
javascript
app.directive('custom', {
mounted(el, binding) {
// 访问组件实例的data、methods
const componentInstance = binding.instance
console.log(componentInstance.msg) // 访问组件的msg数据
componentInstance.handleMethod() // 调用组件的方法
}
})
3. 指令模块化封装
对于大型项目,可将通用指令封装为独立模块,统一管理,便于复用和维护:
javascript
// 1. 新建 directives/index.js(指令入口)
import focus from './focus'
import permission from './permission'
import debounce from './debounce'
// 批量注册全局指令
export default (app) => {
app.directive('focus', focus)
app.directive('permission', permission)
app.directive('debounce', debounce)
}
// 2. main.js 引入
import installDirectives from './directives'
const app = createApp(App)
installDirectives(app) // 批量注册所有指令
app.mount('#app')
七、自定义指令使用注意事项
1. 避免过度使用
自定义指令仅用于底层 DOM 操作,若功能可通过组件、Props、组合式函数实现,优先选择其他方式,避免滥用指令导致代码逻辑混乱。
2. 必须清理资源
在 unbind(Vue2)或 unmounted(Vue3)钩子中,必须清理指令绑定的事件监听器、定时器、观察者等资源,避免内存泄漏(如示例中防抖指令清理定时器)。
3. 不依赖 DOM 结构
指令操作的 DOM 元素可能被动态渲染或删除,需做好容错处理(如使用 el.parentNode?.removeChild(el),避免父节点不存在导致报错)。
4. 区分 Vue2/Vue3 钩子差异
Vue2 中,若需操作已插入 DOM 的元素,需使用 inserted钩子;Vue3 中,对应使用 mounted 钩子,避免因钩子使用错误导致 DOM 操作失效。
5. 支持 TypeScript 类型定义
Vue3 中,可通过扩展 ComponentCustomProperties 接口,为自定义全局指令添加 TypeScript 类型,提升类型安全性和开发体验。
八、总结
Vue 自定义指令的核心是封装底层 DOM 操作逻辑,实现代码复用,其核心用法可总结为:
- 注册方式:全局注册(通用指令)、局部注册(组件专属指令),Vue3 组合式 API 简化了局部注册流程;
- 核心逻辑:通过钩子函数在指令生命周期的不同阶段执行 DOM 操作,钩子参数提供了指令绑定的关键信息;
- 适用场景:焦点控制、权限控制、防抖节流、图片懒加载、输入校验等需直接操作 DOM 的场景;
- 版本差异:重点区分 Vue2 与 Vue3 的钩子函数和注册方式,便于项目迁移和兼容。
本文所有示例均可直接复制到项目中使用,只需根据 Vue 版本调整钩子函数和注册方式,即可快速适配实战需求。合理使用自定义指令,能有效减少冗余代码,提升项目的可维护性和开发效率。