Vue.js: 自定义指令
最后更新:2026-08-26
自定义指令让你扩展 Vue 的模板语法——以 v- 开头的特殊属性,对底层 DOM 进行直接操作。Vue 内置了 v-if / v-for / v-model 等指令,你可以创建自己的 v-focus / v-permission / v-debounce 等。
自定义指令是写"底层 DOM 工具"的强大方式——它把可复用的 DOM 操作封装成声明式语法。理解 5 大生命周期钩子让你能写各种 v-directive。
1. 你将学到
- 自定义指令的本质和 3 大使用场景
- 全局指令
app.directive() - 局部指令
directives: {} - 5 大生命周期钩子(created/beforeMount/mounted/beforeUpdate/unmounted)
- 5 大实战指令(v-focus / v-permission / v-debounce / v-copy / v-lazy-load)
- 指令参数、修饰符、值
- 5 个反模式(滥用、覆盖内置、忘清理等)
2. 一个权限按钮"5 处重复"的噩梦
(1) 痛点:5 个按钮都要检查权限
Alice 的后台有 5 个按钮需要权限检查:
VUE
<!-- ❌ 翻车版:5 个按钮,5 份权限代码 -->
<template>
<button v-if="hasPermission('user.create')" @click="createUser">Create</button>
<button v-if="hasPermission('user.delete')" @click="deleteUser">Delete</button>
<button v-if="hasPermission('user.edit')" @click="editUser">Edit</button>
<button v-if="hasPermission('order.create')" @click="createOrder">Create Order</button>
<button v-if="hasPermission('order.cancel')" @click="cancelOrder">Cancel</button>
</template>
<script setup>
function hasPermission(perm) {
return user.value.permissions?.includes(perm)
}
</script>
5 个按钮 × 5 个权限检查 = 25 行重复代码。新增按钮还要再写 v-if。
产品经理 Charlie:
"Alice,后台有 50+ 个按钮。我们需要一个 'v-permission' 指令让代码更干净。"
(2) Vue 自定义指令解法:1 个 v-permission
JS
// directives/permission.js
export const permission = {
mounted(el, binding) {
const { value } = binding // 'user.create'
const userPermissions = getCurrentUser().permissions || []
if (!userPermissions.includes(value)) {
el.parentNode?.removeChild(el) // 没权限就移除
}
}
}
JS
// main.js
import { permission } from './directives/permission'
app.directive('permission', permission)
VUE
<!-- 使用:1 个 v-permission 替代所有 v-if -->
<template>
<button v-permission="'user.create'" @click="createUser">Create</button>
<button v-permission="'user.delete'" @click="deleteUser">Delete</button>
<button v-permission="'order.create'" @click="createOrder">Create Order</button>
</template>
50 个按钮 50 个 v-permission,比 50 个 v-if 简洁 50%。
(3) 收益
使用自定义指令后:
- 代码量:25 行 v-if → 3 行 v-permission(-88%)
- 新增按钮:1 个 v-permission 替代 1 个 v-if
- 权限逻辑集中:1 个 directives/permission.js
- 可复用:其他项目也能用 v-permission
3. 自定义指令基础
(1) 3 种注册方式
JS
// 1. 全局指令(main.js)
const { createApp } = Vue
import App from './App.vue'
const app = createApp(App)
// 全局注册:所有组件都能用 v-focus
app.directive('focus', {
mounted(el) {
el.focus()
}
})
app.mount('#app')
VUE
<!-- 局部指令(推荐) -->
<!-- src/components/Input.vue -->
<script setup>
// 局部注册:只有这个组件能用
const vFocus = {
mounted(el) {
el.focus()
}
}
</script>
<template>
<input v-focus>
</template>
JS
// 2. 简写(mounted + updated)
app.directive('color', (el, binding) => {
el.style.color = binding.value
})
(2) 5 大生命周期钩子
JS
app.directive('demo', {
// 1. created(指令创建)
created(el, binding) {
console.log('1. 指令创建')
},
// 2. beforeMount(元素挂载前)
beforeMount(el) {
console.log('2. 挂载前')
},
// 3. mounted(元素已挂载)⭐ 最常用
mounted(el, binding) {
console.log('3. 已挂载')
},
// 4. beforeUpdate(依赖更新前)
beforeUpdate(el, binding) {
console.log('4. 更新前')
},
// 5. updated(依赖更新后)
updated(el, binding) {
console.log('5. 已更新')
},
// 6. beforeUnmount(卸载前)
beforeUnmount(el) {
console.log('6. 卸载前')
},
// 7. unmounted(卸载后)⭐ 清理用
unmounted(el) {
console.log('7. 已卸载')
}
})
(3) 钩子参数详解
JS
// el, binding, vnode, prevVnode 4 个参数
mounted(el, binding, vnode, prevVnode) {
// el: 指令绑定的元素
el.style.color = 'red'
// binding: 指令信息对象
binding.value // 指令值,如 v-foo="bar" → bar
binding.arg // 参数,如 v-foo:arg → 'arg'
binding.modifiers // 修饰符,如 v-foo.bar → { bar: true }
binding.instance // 使用指令的组件实例
binding.dir // 指令定义对象
// vnode: Vue 虚拟节点(一般不用)
// prevVnode: 上一个虚拟节点
}
4. 5 大实战指令
(1) v-focus:自动聚焦
JS
// directives/focus.js
export const focus = {
mounted(el, binding) {
if (binding.value !== false) {
el.focus()
}
}
}
VUE
<template>
<!-- 1. 自动聚焦 -->
<input v-focus>
<!-- 2. 条件聚焦 -->
<input v-focus="shouldFocus">
<!-- 3. 延迟聚焦 -->
<input v-focus:delay="500">
</template>
(2) v-permission:权限控制
JS
// directives/permission.js
import { getCurrentUser } from '@/utils/auth'
export const permission = {
mounted(el, binding) {
const { value, modifiers } = binding
const user = getCurrentUser()
// value: 字符串 'user.create' 或 数组 ['user.create', 'user.delete']
// modifiers.disable: 禁用而非移除
const required = Array.isArray(value) ? value : [value]
const hasPermission = required.every(p =>
user.permissions?.includes(p)
)
if (!hasPermission) {
if (modifiers.disable) {
el.disabled = true
el.title = 'No permission'
} else {
el.parentNode?.removeChild(el)
}
}
}
}
VUE
<template>
<!-- 单权限 -->
<button v-permission="'user.create'">Create</button>
<!-- 多权限(全部满足)-->
<button v-permission="['user.read', 'user.write']">Edit</button>
<!-- 没权限时禁用(而非移除)-->
<button v-permission.disable="'user.delete'">Delete</button>
</template>
(3) v-debounce:防抖事件
JS
// directives/debounce.js
export const debounce = {
mounted(el, binding) {
const { value, arg = 300 } = binding
if (typeof value !== 'function') {
console.warn('v-debounce: value must be a function')
return
}
let timer = null
el.__debounceTimer__ = timer
el.addEventListener('click', () => {
clearTimeout(timer)
timer = setTimeout(() => value(), arg)
el.__debounceTimer__ = timer
})
},
unmounted(el) {
if (el.__debounceTimer__) {
clearTimeout(el.__debounceTimer__)
}
}
}
VUE
<template>
<!-- v-debounce 用法:值传函数,arg 传延迟毫秒数 -->
<button v-debounce:500="handleClick">Click me</button>
<input v-debounce:1000="handleInput">
</template>
<script setup>
function handleClick() {
console.log('Clicked (debounced 500ms)')
}
</script>
(4) v-copy:点击复制
JS
// directives/copy.js
export const copy = {
mounted(el, binding) {
const handler = async () => {
try {
await navigator.clipboard.writeText(binding.value)
const original = el.textContent
el.textContent = 'Copied!'
setTimeout(() => { el.textContent = original }, 1500)
} catch (err) {
console.error('Copy failed:', err)
}
}
el.addEventListener('click', handler)
el.__copyHandler__ = handler
},
unmounted(el) {
if (el.__copyHandler__) {
el.removeEventListener('click', el.__copyHandler__)
}
}
}
VUE
<template>
<button v-copy="shareUrl">Copy Link</button>
<code v-copy="apiKey">Click to copy</code>
</template>
(5) v-lazy-load:图片懒加载
JS
// directives/lazyLoad.js
export const lazyLoad = {
mounted(el, binding) {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
el.src = binding.value
observer.unobserve(el)
}
})
observer.observe(el)
el.__observer__ = observer
},
unmounted(el) {
el.__observer__?.disconnect()
}
}
VUE
<template>
<img v-lazy-load="imageUrl" alt="...">
</template>
5. 指令参数、修饰符、值
(1) 3 种指令形态
VUE
<!-- 1. v-directive="value"(值) -->
<input v-foo="username">
<!-- 2. v-directive:arg(参数,固定字符串) -->
<input v-foo:delay="500">
<!-- 3. v-directive.modifier(修饰符,布尔对象) -->
<input v-foo.bar>
<!-- 4. 组合 -->
<input v-foo:delay.bar="500">
(2) JS 接收方式
JS
app.directive('demo', (el, binding) => {
// v-demo="123"
binding.value // 123
// v-demo:abc
binding.arg // 'abc'
// v-demo.foo
binding.modifiers // { foo: true }
// v-demo:abc.foo="123"
binding.value // 123
binding.arg // 'abc'
binding.modifiers // { foo: true }
})
(3) 5 大组合场景
VUE
<!-- 场景 1:v-permission:disable -->
<button v-permission:disable="'user.create'">
<!-- arg='disable', value='user.create' -->
</button>
<!-- 场景 2:v-debounce:500 -->
<button v-debounce:500="handler">
<!-- arg='500'(500ms 防抖) -->
</button>
<!-- 场景 3:v-once.lazy -->
<img v-once.lazy="imageUrl">
<!-- modifiers.lazy=true, value=imageUrl -->
</template>
6. 完整示例:5 大指令 + 实战
▶ 示例:v-focus + v-copy + v-debounce 综合演示
HTML
📖 仅展示
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<style>
input, button { padding: 6px 12px; margin: 4px; border-radius: 4px; border: 1px solid #ddd; }
button { background: #42b883; color: white; border: none; cursor: pointer; }
</style>
<div id="app">
<h4>v-focus(自动聚焦)</h4>
<input v-focus placeholder="打开页面自动聚焦" v-model="text1">
<input v-focus:500 placeholder="500ms 后聚焦" v-model="text2">
<h4>v-copy(点击复制)</h4>
<button v-copy="'https://web-tutorial.com'">复制链接</button>
<p style="color: #999;">复制后按钮会显示"Copied!" 1.5 秒</p>
<h4>v-debounce:800(800ms 防抖)</h4>
<button v-debounce:800="handleSave">保存(防抖 800ms)</button>
<p>实际触发次数: {{ saveCount }}</p>
</div>
<script>
const { createApp, ref } = Vue
// 自定义指令:v-focus(支持 arg 延迟聚焦)
const focusDirective = {
mounted(el, binding) {
if (binding.value === false) return
if (binding.arg) {
setTimeout(() => el.focus(), parseInt(binding.arg))
} else {
el.focus()
}
}
}
// 自定义指令:v-copy(点击复制文本)
const copyDirective = {
mounted(el, binding) {
const original = el.textContent
const handler = async () => {
try {
await navigator.clipboard.writeText(binding.value)
el.textContent = 'Copied!'
setTimeout(() => { el.textContent = original }, 1500)
} catch (err) {
console.error('Copy failed:', err)
}
}
el.addEventListener('click', handler)
el._copyHandler = handler
},
unmounted(el) {
if (el._copyHandler) el.removeEventListener('click', el._copyHandler)
}
}
// 自定义指令:v-debounce(防抖事件)
const debounceDirective = {
mounted(el, binding) {
const fn = binding.value
const delay = parseInt(binding.arg) || 300
if (typeof fn !== 'function') {
console.warn('[v-debounce] value must be a function')
return
}
let timer = null
el.addEventListener('click', () => {
clearTimeout(timer)
timer = setTimeout(() => fn(), delay)
})
el._debounceTimer = timer
},
unmounted(el) {
if (el._debounceTimer) clearTimeout(el._debounceTimer)
}
}
const app = createApp({
setup() {
const text1 = ref('')
const text2 = ref('')
const saveCount = ref(0)
function handleSave() { saveCount.value++ }
return { text1, text2, saveCount, handleSave }
}
})
app.directive('focus', focusDirective)
app.directive('copy', copyDirective)
app.directive('debounce', debounceDirective)
app.mount('#app')
</script>
▶ 示例:5 个常见错误速查
| 错误 | 现象 | 解决 |
|---|---|---|
| 指令名不带 v- | 不生效 | v-focus(不是 focus) |
| value 不是函数 | 不执行 | 检查 v-debounce="handler" |
| 忘 unmounted 清理 | 内存泄漏 | 定时器/观察者要清理 |
| 覆盖 Vue 内置指令 | 报错 | 不叫 v-if 等 |
| 指令值修改不响应 | 只 mounted 一次 | 用 updated 钩子 |
▶ 示例:5 大性能对比
| 写法 | 复用 | 性能 | 适用 |
|---|---|---|---|
v-if + 工具函数 |
❌ | ⭐⭐⭐ | 1 次性 |
| 自定义指令 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | DOM 工具 |
| 全局组件 | ⭐⭐⭐ | ⭐⭐⭐ | 复杂 UI |
| Composable | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 业务逻辑 |
| Pinia | ⭐⭐⭐⭐ | ⭐⭐⭐ | 全局状态 |
▶ 示例:5 大内置指令借鉴(app.directive)
JS
const { createApp } = Vue
const app = createApp({})
// 1. v-show(控制 display)
app.directive('show', {
mounted(el, binding) { el.style.display = binding.value ? '' : 'none' },
updated(el, binding) { el.style.display = binding.value ? '' : 'none' }
})
// 2. v-text(设置 textContent)
app.directive('text', {
mounted(el, binding) { el.textContent = binding.value },
updated(el, binding) { el.textContent = binding.value }
})
// 3. v-html(设置 innerHTML,XSS 风险)
app.directive('html', {
mounted(el, binding) { el.innerHTML = binding.value },
updated(el, binding) { el.innerHTML = binding.value }
})
❓ 常见问题
Q 自定义指令和组件怎么选?
A DOM 底层操作(focus/scroll/canvas)用指令。复杂 UI(带状态/事件/数据)用组件。指令是无状态的,组件是有状态的。
Q 指令的 value 是响应式的吗?
A value 改变会触发 updated 钩子。如果要监听 value 变化,用
watch(binding.value) 或在 updated 钩子中对比新旧值。Q v-permission:disable 怎么写?
A arg='disable',modifiers.disable=true。在指令中判断
binding.modifiers.disable 决定禁用还是移除。Q 指令能用 TypeScript 吗?
A 能。Vue 3.3+ 支持
defineDirective + TypeScript 泛型。但通常直接用 JS 对象更简单。Q 指令的全局 vs 局部怎么选?
A 业务专属(v-permission)用局部。通用工具(v-focus / v-copy)用全局。本教程推荐局部(更易维护)。
Q 指令能传 ref 吗?
A 能。
<input v-my-directive="myRef" />,指令中 binding.value 就是 ref 对象。但不推荐(反模式)。Q VueUse 库有指令吗?
A 有。
vFocus / vClickOutside / vLazyLoad / vInfiniteScroll 等都是 VueUse 提供的指令。优先用 VueUse 而非自己写。📖 小节
- 自定义指令是 v- 开头的特殊属性,用于直接操作 DOM
- 3 种注册:全局(app.directive)/ 局部(directives: {})/ 简写(mounted+updated 合并)
- 5 大生命周期钩子:created / beforeMount / mounted / beforeUpdate / updated / beforeUnmount / unmounted
- 5 大实战:v-focus / v-permission / v-debounce / v-copy / v-lazy-load
- 3 种形态:值 / 参数(arg)/ 修饰符(modifiers)
- 5 个反模式:忘 v- 前缀 / value 非函数 / 忘清理 / 覆盖内置 / 不响应 value 变化
📝 作业
-
基础题(难度⭐) 实现 v-focus 指令:
- 接受 arg 参数(延迟聚焦毫秒数)
- 接受 modifiers 修饰符(prevent:阻止默认行为)
-
进阶题(难度⭐⭐) 实现 v-permission 完整版:
- 支持单权限字符串:v-permission="'user.create'"
- 支持多权限数组:v-permission="['user.read', 'user.write']"
- 支持修饰符:.disable(禁用而非移除)
- 配合 currentUser(provide/inject 注入)
-
挑战题(难度⭐⭐⭐) 实现完整的"指令库":
- 5 个指令:v-focus / v-permission / v-debounce / v-copy / v-lazy-load
- 每个指令完整实现 + TypeScript 类型
- 统一在 directives/index.js 导出
- 5 个测试用例(用 Vitest)
- 在实际场景中使用(电商后台 5 个不同场景)
- 文档化每个指令的 API 和参数