Vue 3 自定义指令完全指南
本文档从零开始讲解 Vue 3 的自定义指令:什么是自定义指令、为什么需要、有哪些钩子、如何注册和使用、如何传参和用修饰符,以及多个完整示例(聚焦、颜色、权限、加载、防抖、点击外部、复制、长按等),适合新手系统学习。
一、什么是“自定义指令”?
1.1 回顾:内置指令
你已经用过 Vue 的内置指令:v-if、v-for、v-model、v-show 等。
它们都是“以 v- 开头的特殊属性”,用来直接操作 DOM 或绑定行为。
例如 v-show 会改元素的 display,v-model 会绑定 value 和 input 事件。
1.2 自定义指令是什么?
自定义指令就是你自己定义的、以 v- 开头的指令。
当 Vue 在模板里遇到你写的 v-xxx 时,会在特定时机(挂载、更新、卸载等)调用你提供的函数,你可以在这些函数里直接操作 DOM(改样式、聚焦、加事件监听等)。
1.3 什么时候用自定义指令?
- 需要反复对 DOM 做同一类操作(如自动聚焦、高亮、权限隐藏、复制到剪贴板),又不想在每个组件里重复写逻辑时。
- 操作是“针对这个元素本身”的(改这个 input 的 focus、这个 div 的颜色),而不是“渲染一块子结构”;后者更适合用组件。
简单记:
- 改结构、复用一块 UI → 用组件。
- 改某个已有元素的 DOM 行为/样式 → 用自定义指令更合适。
二、指令的“钩子”是什么?
Vue 会在元素的不同阶段调用你提供的函数,这些函数叫做钩子。
你只要在“需要的时候”写对应钩子即可,不必全部写满。
| 钩子名 | 调用时机 |
|---|---|
| created | 元素创建后、挂载到 DOM 前(此时还拿不到父节点) |
| beforeMount | 元素即将挂载到 DOM 前 |
| mounted | 元素已经挂载到 DOM 后(最常用:操作 DOM、加事件监听) |
| beforeUpdate | 所在组件状态变化、即将重新渲染前 |
| updated | 重新渲染完成后(常用:依赖新数据再改 DOM) |
| beforeUnmount | 元素即将从 DOM 移除前 |
| unmounted | 元素已从 DOM 移除后(常用:解绑事件、清定时器) |
新手最常用:
- mounted:第一次把元素放到页面上时执行,适合聚焦、设颜色、加监听。
- updated:指令绑定的值变了,组件更新完后执行,适合根据新值再改 DOM。
- unmounted:元素被移除时执行,适合清理。
三、钩子函数里能拿到什么?
每个钩子都会收到这几个参数:
(el, binding, vnode, prevVnode) => { }
- el:指令绑定的 DOM 元素,可以直接操作(如
el.focus()、el.style.color = ...)。 - binding:一个对象,包含指令的值、参数、修饰符等(见下一节)。
- vnode:当前元素的 VNode(Vue 内部用,一般少改)。
- prevVnode:上一次的 VNode(只在 beforeUpdate、updated 里有)。
下面重点说 binding。
3.1 binding 对象里有什么?
假设模板里这样写:
<div v-my-dir:arg.modifier="value"></div>
binding 里大致有:
| 属性 | 含义 | 上例中 |
|---|---|---|
| value | 等号右边的值(即指令的“值”) | 就是 value 这个变量 |
| oldValue | 上一次的值(只在 update 相关钩子里有) | 更新前的值 |
| arg | 冒号后面的“参数” | 'arg' |
| modifiers | 点后面的修饰符组成的对象 | { modifier: true } |
| instance | 当前组件实例 | 当前组件的 proxy |
| dir | 指令定义对象本身 | 你写的那个 directive 对象 |
简单示例:
<p v-color="'red'">红色文字</p>
<p v-color:bg="'blue'">蓝色背景</p>
在指令里:
binding.value是'red'或'blue'。- 第二个有 arg:
binding.arg === 'bg'。
四、如何定义和注册指令?
4.1 全局注册(整个应用都能用)
在 main.js(或创建 app 的地方)里:
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.directive('focus', {
mounted(el) {
el.focus()
}
})
app.mount('#app')
之后在任意组件的模板里都可以写 v-focus。
4.2 局部注册(只在当前组件用)
在组件的 里,定义一个以 v 开头的变量,值为指令对象或函数:
<template>
<input v-focus />
</template>
<script setup>
const vFocus = {
mounted(el) {
el.focus()
}
}
</script>
变量名 vFocus 对应模板里的 v-focus(驼峰对应短横线)。
4.3 简写:只关心 mounted 和 updated
若你只在挂载和更新时做同一件事,可以传一个函数,Vue 会把它同时当作 mounted 和 updated 的钩子:
app.directive('color', (el, binding) => {
el.style.color = binding.value
})
等价于:
app.directive('color', {
mounted(el, binding) {
el.style.color = binding.value
},
updated(el, binding) {
el.style.color = binding.value
}
})
五、基础示例
5.1 v-focus:自动聚焦
需求: 页面打开或某个输入框显示时,自动获得焦点。
// main.js 或组件内
app.directive('focus', {
mounted(el) {
el.focus()
}
})
<input v-focus placeholder="自动聚焦" />
注意: 若元素是 v-if 控制的,可能要在 nextTick 里再 focus,确保已经插入 DOM:
mounted(el) {
setTimeout(() => el.focus(), 0)
// 或
// import { nextTick } from 'vue'
// nextTick(() => el.focus())
}
5.2 v-color:根据值设置文字颜色
需求: 传一个颜色名或色值,把元素的文字颜色设为该值。
app.directive('color', {
mounted(el, binding) {
el.style.color = binding.value
},
updated(el, binding) {
el.style.color = binding.value
}
})
<p v-color="'red'">红色</p>
<p v-color="colorVar">动态颜色</p>
若希望支持“文字颜色”和“背景颜色”,可以用 参数 arg:
app.directive('color', {
mounted(el, binding) {
if (binding.arg === 'bg') {
el.style.backgroundColor = binding.value
} else {
el.style.color = binding.value
}
},
updated(el, binding) {
if (binding.arg === 'bg') {
el.style.backgroundColor = binding.value
} else {
el.style.color = binding.value
}
}
})
<p v-color="'blue'">蓝色文字</p>
<p v-color:bg="'lightblue'">浅蓝背景</p>
5.3 v-color 简写形式(函数写法)
app.directive('color', (el, binding) => {
if (binding.arg === 'bg') {
el.style.backgroundColor = binding.value
} else {
el.style.color = binding.value
}
})
六、进阶示例
6.1 v-permission:根据权限显示/隐藏
需求: 传一个权限码,用户没有该权限时,把元素隐藏或移除。
// 假设从某处拿到当前用户权限列表
function getPermissions() {
return ['user:view', 'user:edit']
}
app.directive('permission', {
mounted(el, binding) {
const permissions = getPermissions()
const need = binding.value
if (!permissions.includes(need)) {
el.style.display = 'none'
// 或直接移除:el.parentNode?.removeChild(el)
}
}
})
<button v-permission="'user:edit'">编辑</button>
实际项目中 getPermissions 可能从 store、composable 或 inject 里拿。
6.2 v-loading:加载中遮罩
需求: 传 true/false,为 true 时在元素上盖一层“加载中”的遮罩。
app.directive('loading', {
mounted(el, binding) {
if (!binding.value) return
const mask = document.createElement('div')
mask.className = 'v-loading-mask'
mask.innerHTML = '<span>加载中...</span>'
mask.style.cssText = `
position: absolute; inset: 0; background: rgba(255,255,255,0.8);
display: flex; align-items: center; justify-content: center;
`
el.style.position = 'relative'
el.appendChild(mask)
el._loadingMask = mask
},
updated(el, binding) {
const mask = el._loadingMask
if (!mask) return
if (binding.value) {
if (!el.contains(mask)) el.appendChild(mask)
mask.style.display = 'flex'
} else {
mask.style.display = 'none'
}
},
unmounted(el) {
el._loadingMask?.remove()
}
})
<div v-loading="loading" style="height: 200px;">
内容区域
</div>
const loading = ref(true)
这里用 el._loadingMask 存了创建的 DOM,在 updated/unmounted 里复用或移除,避免重复创建。
6.3 v-debounce:防抖点击
需求: 点击后不要立即执行,而是等一段时间内没有再次点击才执行(防抖)。
app.directive('debounce', {
mounted(el, binding) {
const delay = binding.value ?? 300
let timer = null
el._debounceHandler = (e) => {
clearTimeout(timer)
timer = setTimeout(() => {
binding.instance?.$emit?.('click', e)
if (typeof binding.value === 'function') {
binding.value(e)
}
}, delay)
}
el.addEventListener('click', el._debounceHandler)
},
unmounted(el) {
el.removeEventListener('click', el._debounceHandler)
}
})
更简单的一种是:指令只负责“节流/防抖”包装用户传的函数,例如传一个函数作为 value,在指令里对调用做防抖(上面示例混合了事件和函数,仅作思路参考)。
实际更推荐在组件里用 @click + 自己封装的 useDebounceFn 或工具函数,指令版可在“只想写 v-debounce 不改模板逻辑”时使用。
6.4 v-click-outside:点击元素外部时触发
需求: 用户点击到元素外面时,执行一个回调(如关闭下拉、关闭弹窗)。
app.directive('click-outside', {
mounted(el, binding) {
el._clickOutside = (e) => {
if (!el.contains(e.target)) {
binding.value?.(e)
}
}
document.addEventListener('click', el._clickOutside)
},
unmounted(el) {
document.removeEventListener('click', el._clickOutside)
}
})
<div v-click-outside="close">点击我外部会触发 close</div>
close 在组件里定义为一个函数。
unmounted 里一定要移除监听,避免内存泄漏和误触发。
6.5 v-copy:点击复制到剪贴板
需求: 点击元素时,把某段文字复制到剪贴板,并可选提示“已复制”。
app.directive('copy', {
mounted(el, binding) {
el._copyHandler = async () => {
const text = binding.value ?? el.textContent
try {
await navigator.clipboard.writeText(text)
if (binding.modifiers.toast) {
alert('已复制')
}
} catch (err) {
console.error('复制失败', err)
}
}
el.addEventListener('click', el._copyHandler)
},
unmounted(el) {
el.removeEventListener('click', el._copyHandler)
}
})
<span v-copy.toast="'要复制的文字'">点击复制</span>
<span v-copy>复制这段文字内容</span>
- value 为要复制的字符串;不传则用 el.textContent。
- .toast 修饰符表示复制后 alert 提示(实际项目可换成你的 toast 组件)。
6.6 v-longpress:长按触发
需求: 按住超过一定时间才触发,而不是点一下就触发。
app.directive('longpress', {
mounted(el, binding) {
const duration = binding.value ?? 500
let timer = null
const start = () => {
timer = setTimeout(() => {
binding.value?.()
}, duration)
}
const cancel = () => clearTimeout(timer)
el._longpressStart = start
el._longpressCancel = cancel
el.addEventListener('mousedown', start)
el.addEventListener('mouseup', cancel)
el.addEventListener('mouseleave', cancel)
el.addEventListener('touchstart', start)
el.addEventListener('touchend', cancel)
},
unmounted(el) {
el.removeEventListener('mousedown', el._longpressStart)
el.removeEventListener('mouseup', el._longpressCancel)
el.removeEventListener('mouseleave', el._longpressCancel)
el.removeEventListener('touchstart', el._longpressStart)
el.removeEventListener('touchend', el._longpressCancel)
}
})
<button v-longpress="onLongPress">长按 500ms 触发</button>
<button v-longpress="800">长按 800ms(传的是数字,需在指令里区分类型或约定)</button>
这里约定 value 为函数或数字(数字为时长);若既要传时长又要传回调,可用对象:v-longpress="{ duration: 800, handler: onLongPress }",在指令里解析。
七、参数(arg)与修饰符(modifiers)小结
7.1 用参数区分“模式”
例如 v-color:bg 表示设背景色,v-color 表示设文字颜色:
- binding.arg 为
'bg'或undefined。 - 在钩子里根据 arg 写不同逻辑(见 5.2、5.3)。
7.2 用修饰符做开关
例如 v-copy.toast 表示复制后提示:
- binding.modifiers 为
{ toast: true }。 - 在钩子里判断 binding.modifiers.toast 再决定是否弹提示。
7.3 值、参数、修饰符一起用
<div v-dir:arg.mod1.mod2="value"></div>
- binding.value:value
- binding.arg:’arg’
- binding.modifiers:{ mod1: true, mod2: true }
八、易错点与注意点
8.1 一定要在 unmounted 里清理
若在 mounted 里加了全局或 document 的监听、定时器、创建的 DOM,要在 unmounted 里移除监听、清定时器、移除 DOM,否则组件销毁后仍会执行,造成内存泄漏或报错。
8.2 操作的是真实 DOM
指令钩子里的 el 是真实 DOM 元素,不是组件实例。
要改样式、属性、子节点,都用 el.xxx;不要指望在指令里拿到“子组件”的 ref,除非 el 是自定义组件的根 DOM。
8.3 值变化会触发 updated
binding.value 变化时(例如父组件传的变量变了),会先触发组件更新、再触发指令的 beforeUpdate / updated。
若你的逻辑依赖“新值”,写在 updated 里即可。
8.4 何时用指令、何时用组件
- 指令:对单个已有元素做 DOM 行为或样式(聚焦、颜色、权限隐藏、复制、长按、防抖点击、点击外部)。
- 组件:一块可复用的 UI 结构 + 逻辑(按钮、卡片、表单)。
若既要改 DOM 又要包一层复杂结构,优先考虑组件;若只是“给现有元素加一种行为”,用指令更合适。
九、自定义指令速查表
| 需求 | 做法 |
|---|---|
| 定义钩子 | mounted(el, binding) { }、updated、unmounted 等 |
| 拿指令的值 | binding.value |
| 拿参数 | binding.arg(如 v-dir:arg 中的 arg) |
| 拿修饰符 | binding.modifiers(如 v-dir.mod 中 modifiers.mod === true) |
| 全局注册 | app.directive('name', { mounted, updated, ... }) |
| 局部注册 | const vName = { mounted, ... }(script setup 中) |
| 简写 | 只写一个函数,同时作为 mounted 和 updated |
十、学习建议
- 先会写一个钩子(常用 mounted),在里边用 el 和 binding.value 做简单操作(如 focus、改 color)。
- 再练 updated(值变时再改 DOM)和 unmounted(移除监听、清理)。
- 需要“多种用法”时用 binding.arg;需要“开关”时用 binding.modifiers。
- 凡是加了全局监听、定时器、自建 DOM,一定要在 unmounted 里清理。
把本文档里的 v-focus、v-color、v-click-outside、v-copy 在项目里敲一遍,再试着改 value/arg/modifiers,会掌握得更牢。内置指令见《Vue3指令.md》。祝你学习顺利。