Vue.js: 自定义指令
最后更新:2026-10-20
自定义指令让你扩展 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 个按钮需要权限检查:
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)
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
// 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: 上一个虚拟节点
}
▶ 示例:app.directive 注册 v-highlight(用到 value 与 modifiers)
▶ 试一试输出:
TEXT
顶部三个色块按钮「#ffe08a / #a5d8ff / #b2f2bb」,下方两行文字初始都是淡黄色底,第二行加粗。
点「#a5d8ff」→ 两行背景同时变成淡蓝色,第一行保持常规字重、第二行依然加粗——说明 .bold 修饰符只影响挂了它的那个元素。
再点「#b2f2bb」→ 两行一起变成淡绿色。
▶ 示例: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 }
})
输出:
TEXT
这段代码只定义并注册指令,没有挂载任何页面,所以页面上看不到变化。
实际使用时:v-show="false" 的元素 display 变成 none;v-text="'hi'" 写入纯文本 hi;v-html="'<b>x</b>'" 渲染出加粗的 x。
4. 5 大实战指令
(1) v-focus:自动聚焦
JS
// directives/focus.js
export const focus = {
mounted(el, binding) {
if (binding.value !== false) {
el.focus()
}
}
}
(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)
}
}
}
}
(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__)
}
}
}
(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__)
}
}
}
(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()
}
}
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. 综合演练:三大指令联动
▶ 示例:v-focus + v-copy + v-debounce 综合演示
▶ 展开运行示例(80 行)
输出:
TEXT
三个区块自上而下:
v-focus — 两个输入框,打开页面时第一个(占位符「打开页面自动聚焦」)已获得焦点,500ms 后焦点自动跳到第二个;
v-copy — 按钮「复制链接」,点击后文字变「Copied!」,1.5 秒后恢复,剪贴板里是 https://web-tutorial.com;
v-debounce:800 — 按钮「保存(防抖 800ms)」和「实际触发次数: 0」,连点多次只在停止点击 800ms 后计一次,次数变 1。
(1) 📊 5 个常见错误速查
| 错误 | 现象 | 解决 |
|---|---|---|
| 指令名不带 v- | 不生效 | v-focus(不是 focus) |
| value 不是函数 | 不执行 | 检查 v-debounce="handler" |
| 忘 unmounted 清理 | 内存泄漏 | 定时器/观察者要清理 |
| 覆盖 Vue 内置指令 | 报错 | 不叫 v-if 等 |
| 指令值修改不响应 | 只 mounted 一次 | 用 updated 钩子 |
(2) 📊 5 大性能对比
| 写法 | 复用 | 性能 | 适用 |
|---|---|---|---|
v-if + 工具函数 |
❌ | ⭐⭐⭐ | 1 次性 |
| 自定义指令 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | DOM 工具 |
| 全局组件 | ⭐⭐⭐ | ⭐⭐⭐ | 复杂 UI |
| Composable | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | 业务逻辑 |
| Pinia | ⭐⭐⭐⭐ | ⭐⭐⭐ | 全局状态 |
❓ 常见问题
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 和参数