指令注册:全局注册 vs 局部注册,指令命名规范,指令的钩子函数

好,咱们接着聊指令的注册方式。说实话,这块内容不难,但特别容易踩坑。我刚开始用 Vue 自定义指令那会儿,就因为在全局注册和局部注册之间来回切换,搞得项目里一堆混乱。今天咱们一次性把它理清楚。

一、全局注册 vs 局部注册

先说说最核心的区别:全局注册的指令,在整个应用里都能用;局部注册的指令,只能在当前组件里用。听起来很简单对吧?但实际项目中怎么选,这里头有讲究。

1. 全局注册

全局注册通常放在 main.js 或者一个专门的 directives/index.js 文件里。我个人习惯用 app.directive() 方法:

// main.js
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

// 全局注册一个 v-focus 指令
app.directive('focus', {
  mounted(el) {
    el.focus()
  }
})

app.mount('#app')

你看,这样注册完,项目里任何一个组件的模板都能直接用 v-focus。方便是真方便,但有个问题——全局注册的指令不会被 tree-shaking。什么意思呢?就算你只在一个组件里用了这个指令,打包的时候它还是会打进所有 chunk 里。

注意:全局注册要克制。我曾经接手过一个项目,main.js 里注册了十几个全局指令,结果大部分只在两三个页面里用到。这种场景下,局部注册其实更合适。

2. 局部注册

局部注册就是在组件内部通过 directives 选项来定义:

<script>
export default {
  directives: {
    focus: {
      mounted(el) {
        el.focus()
      }
    }
  }
}
</script>

如果是用 Composition API 的 <script setup> 写法,那就更简洁了:

<script setup>
// 以 v 开头的变量会自动注册为指令
const vFocus = {
  mounted: (el) => el.focus()
}
</script>

嗯,这里有个小细节要注意:<script setup> 里,变量名必须用驼峰命名,并且以 v 开头。比如 vFocusvHighlight。模板里使用时,会自动转换成 v-focusv-highlight

对比维度 全局注册 局部注册
作用范围 整个应用 当前组件
打包体积 无法 tree-shaking 按需打包
适用场景 通用功能(如权限、拖拽) 组件内部专用逻辑
维护成本 低(一次注册到处用) 高(每个组件都要注册)
我的建议:如果是工具类指令(比如防抖、节流、权限校验),优先全局注册。如果是跟组件业务强相关的指令(比如某个表单特有的校验逻辑),用局部注册。

二、指令命名规范

命名这块,Vue 官方有约定,但很多新手容易忽略。说白了就几条规则:

  • 必须用短横线命名:模板里用 v-permission-check,而不是 vPermissionCheck
  • 不要用 v- 前缀命名:注册时写 'focus',Vue 会自动加上 v-
  • 避免跟内置指令冲突:别用 v-ifv-showv-model 这些名字
  • 语义化:指令名要能表达它的功能,比如 v-debouncev-copyv-lazy

我记得有一次,团队里有人注册了一个叫 v-for 的指令,结果页面直接崩了。你想想看,Vue 内置的 v-for 是渲染列表的,你把它覆盖了,那还得了?

最佳实践:指令名最好用「功能+行为」的组合。比如 v-click-outside(点击外部)、v-long-press(长按)、v-infinite-scroll(无限滚动)。这样别人一看名字就知道是干嘛的。

三、指令的钩子函数

Vue 3 的指令钩子跟组件生命周期钩子很像,但名字和触发时机略有不同。咱们一个一个来看:

1. created

在元素的属性或事件监听器被应用之前调用。说白了就是指令刚绑定到元素上,但还没做任何处理的时候。这个钩子用得不多,我一般用来做一些初始化配置。

2. mounted

这是最常用的钩子。元素被挂载到 DOM 上之后触发。比如自动聚焦、绑定事件监听器,都放这里:

const vHighlight = {
  mounted(el, binding) {
    el.style.backgroundColor = binding.value || 'yellow'
  }
}

3. updated

组件的 VNode 更新时触发,但注意——子组件可能还没更新完。如果你需要等子组件也更新完,用 nextTick 包一下:

const vResize = {
  updated(el, binding) {
    nextTick(() => {
      // 确保子组件也渲染完了
      el.style.width = binding.value + 'px'
    })
  }
}

4. beforeUnmount

在元素被卸载之前调用。这个钩子特别重要——用来清理副作用。比如移除事件监听、清除定时器:

const vInterval = {
  mounted(el, binding) {
    el._timer = setInterval(() => {
      console.log('tick')
    }, binding.value)
  },
  beforeUnmount(el) {
    clearInterval(el._timer)  // 不清理的话,组件卸载了定时器还在跑
  }
}
我曾经踩过的坑:写了一个 v-debounce 指令,在 mounted 里绑定了 input 事件,但忘了在 beforeUnmount 里解绑。结果页面切换后,输入框没了,但事件还在监听,控制台疯狂报错。嗯,从那以后我写指令必加清理逻辑。

5. 钩子函数的参数

每个钩子都会收到几个参数,咱们用个表格总结一下:

参数 说明 示例
el 指令绑定的 DOM 元素 el.focus()
binding 包含指令信息的对象 binding.valuebinding.arg
vnode Vue 编译生成的虚拟节点 一般用不到
prevVnode 上一个虚拟节点(仅 updated 钩子有) 对比新旧值

binding 对象里又包含几个常用属性:

  • value:传给指令的值,比如 v-highlight="'red'" 的 value 就是 'red'
  • arg:指令的参数,比如 v-tooltip:top 的 arg 就是 'top'
  • modifiers:修饰符对象,比如 v-click-outside.once 的 modifiers 就是 { once: true }
小技巧:利用 binding.argbinding.modifiers,可以写出非常灵活的指令。比如 v-tooltip:bottom.dark,arg 控制位置,modifiers 控制样式风格。这样一条指令就能覆盖多种场景。

好了,关于指令注册和钩子函数,咱们就聊到这儿。说白了,全局注册图方便,局部注册图精准。钩子函数里,mountedbeforeUnmount 是黄金搭档,一个负责初始化,一个负责清理。下节课咱们会深入讲指令的传参和动态绑定,到时候你会发现,指令的玩法比想象中多得多。