Vue 3 推出 script setup 语法糖之后,组件代码变得简洁了很多,变量和函数不需要再通过 setup 函数 return 出去就能直接在模板中使用。不过这种便捷也带来了新的疑问:以前在 setup 函数里通过 onMounted 注册逻辑,或者通过 directives 选项注册自定义指令,现在换成 script setup 之后,指令该怎么写?很多人第一反应是去写 v-my-directive,结果发现根本没法直接定义。实际上 script setup 对自定义指令有一套自己的约定,理解了这套约定之后你会发现它比选项式写法还要方便。

script setup 中指令的自动注册机制
Vue 3 官方文档中有一条容易被忽略的规则:任何以 v 开头命名的驼峰式变量,都会被 script setup 识别为自定义指令,自动完成局部注册。也就是说,你不需要写 directives 选项,也不需要额外调用 app.directive,只要变量命名符合 vXxx 的格式,模板里就能直接使用对应的 v-xxx 指令。
看一个最典型的例子,实现一个页面加载后自动聚焦的 v-focus 指令:
<script setup>
import { ref } from 'vue'
// 变量名必须是 vFocus 这种 v 开头的驼峰命名
const vFocus = {
mounted(el) {
el.focus()
}
}
const inputValue = ref('')
</script>
<template>
<input v-focus v-model="inputValue" placeholder="我会自动聚焦" />
</template>
```
这段代码里,vFocus 会被编译器识别为指令 v-focus。命名转换规则和组件注册一致:驼峰命名 vMyDirective 对应模板中的 v-my-directive,vDebounceClick 对应 v-debounce-click。如果你发现指令不生效,第一时间应该检查变量名是不是忘了加 v 前缀,或者把驼峰写成了全小写导致匹配失败。
还有一种更精简的写法,当指令逻辑简单时,可以直接把函数赋值给变量,这个函数等价于 mounted 和 updated 钩子的组合:
<script setup>
// 简写形式:函数会在 mounted 和 updated 时都被调用
const vColor = (el, binding) => {
el.style.color = binding.value
}
</script>
<template>
<p v-color="'#e74c3c'">这段文字是红色的</p>
</template>
```
指令钩子函数与参数详解
写指令之前必须先搞清楚钩子函数的执行时机。Vue 3 的指令钩子和组件生命周期是对应的,但名字略有差异,完整的钩子列表包括 created、beforeMount、mounted、beforeUpdate、updated、beforeUnmount、unmounted。最常用的是 mounted(DOM 已插入,可以操作元素)和 updated(组件更新后触发,适合根据新值同步状态)。
每个钩子函数接收固定的参数:第一个参数 el 是指令绑定的 DOM 元素,可以直接操作;第二个参数 binding 是一个对象,包含 value(指令的值)、oldValue(上一个值,仅在 updated 中可用)、arg(指令参数,即 v-my-dir:foo 中的 foo)、modifiers(修饰符对象,即 v-my-dir.prevent 对应的 { prevent: true })。第三个和第四个参数分别是虚拟节点和上一个虚拟节点,日常开发中较少用到。
下面这个权限控制指令综合运用了这些参数,只有拥有对应权限的用户才能看到按钮:
<script setup>
const vPermission = {
mounted(el, binding) {
// 假设从 Pinia 或者接口拿到了当前用户权限列表
const userPermissions = ['edit', 'delete']
const needPermission = binding.value
if (!userPermissions.includes(needPermission)) {
// 没有权限时直接移除该元素
el.parentNode && el.parentNode.removeChild(el)
}
}
}
</script>
<template>
<button v-permission="'edit'">编辑</button>
<button v-permission="'export'">导出</button>
</template>
```
需要注意的是,在 beforeMount 阶段虽然能拿到 el,但元素还没有插入文档,此时调用 el.parentNode 会得到 null,所以涉及父节点操作的逻辑要放在 mounted 里。另外 binding.value 可以是任意类型的值,包括对象和函数,这让指令的表达能力非常灵活,比如传入一个配置对象来控制行为。
从外部文件导入指令与全局注册
指令逻辑写在组件内部虽然直观,但很多指令是通用的,比如防抖、节流、水印、拖拽,这些应该抽离到独立文件中统一维护。在 script setup 中,导入的变量只要命名符合规范,同样会被自动识别为指令。先创建一个 directives 目录存放指令文件:
// src/directives/debounce.js
export const vDebounce = {
mounted(el, binding) {
const delay = binding.arg ? Number(binding.arg) : 300
let timer = null
el._handleClick = function () {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
binding.value()
}, delay)
}
el.addEventListener('click', el._handleClick)
},
unmounted(el) {
// 组件卸载时移除事件监听,避免内存泄漏
el.removeEventListener('click', el._handleClick)
}
}
```
在组件中使用时直接 import 进来即可,导入的名字保持 vDebounce,script setup 会自动完成注册:
<script setup>
import { vDebounce } from '@/directives/debounce'
const submit = () => {
console.log('提交表单')
}
</script>
<template>
<!-- 500 表示防抖延迟为 500ms -->
<button v-debounce:500="submit">提交</button>
</template>
```
如果某个指令在项目里几乎所有页面都会用到,每个组件都导入一遍就很繁琐,这时候可以在入口文件做全局注册。全局注册使用 app.directive,注意全局注册时的指令名不需要带 v 前缀:
// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
// 全局注册,名字不带 v 前缀,模板中使用 v-watermark
app.directive('watermark', {
mounted(el, binding) {
const text = binding.value || '机密文件'
const canvas = document.createElement('canvas')
canvas.width = 200
canvas.height = 150
const ctx = canvas.getContext('2d')
ctx.rotate(-20 * Math.PI / 180)
ctx.font = '16px serif'
ctx.fillStyle = 'rgba(0, 0, 0, 0.15)'
ctx.fillText(text, 20, 100)
el.style.background = `url(${canvas.toDataURL()})`
}
})
app.mount('#app')
```
两种方式各有适用场景:局部导入的方式按需加载、依赖清晰,适合功能较独立或需要传参定制化的指令;全局注册一步到位,适合水印、权限这类横切关注点性质的指令。一个经验法则是,先按局部导入来写,当发现同一个指令被复制到五六个组件里时,再考虑提升为全局指令。
常见坑点与最佳实践
第一个常见的坑是钩子里忘记清理副作用。如果指令中添加了事件监听器、定时器或者 IntersectionObserver,必须在 unmounted 钩子中清理掉,否则组件销毁后这些资源依然存在,长列表滚动或频繁切换页面时会明显感觉内存上涨。上面的防抖指令就是一个标准示范。
第二个坑是在指令内部直接修改组件状态。指令的设计定位是复用 DOM 操作逻辑,它拿到的是真实的 DOM 元素而不是组件实例,Vue 3 中也不再有 this 指向。如果指令需要和组件通信,推荐通过 binding.value 传入回调函数,让指令在特定时机调用回调,状态修改仍然由组件自己完成,这样职责边界更清晰。
最后一点是关于 TypeScript 项目。给指令加上类型定义可以获得更好的提示体验,可以借助 Vue 提供的 Directive 类型:
import type { Directive } from 'vue'
export const vLoading: Directive<HTMLElement, boolean> = {
mounted(el, binding) {
if (binding.value) {
el.style.position = 'relative'
// 这里可以插入一个 loading 遮罩元素
}
},
updated(el, binding) {
// 值变化时切换显示状态
el.style.opacity = binding.value ? '0.6' : '1'
}
}
```
总的来说,script setup 模式下的自定义指令遵循“命名即注册”的原则:以 v 开头的驼峰变量,无论在组件内定义还是从外部导入,都会被自动识别。掌握这套规则之后,配合合理的目录拆分和全局注册策略,就能把指令这种轻量级的逻辑复用手段用得得心应手,让模板代码保持简洁的同时不失表达力。
Vue 3script setup自定义指令修改时间:2026-09-07 20:22:47