人人都会AI编程

10.5 自定义指令:全局注册、局部注册、生命周期钩子、典型应用场景

更新时间:2026-07-09

Vue 内置的指令(v-modelv-ifv-show 等)覆盖了大部分常见的 DOM 操作需求。但当你需要在组件中复用某种 直接操作 DOM 的逻辑时,自定义指令就派上了用场。比如自动聚焦输入框、监听点击目标元素外部、或对一个按钮做防抖处理——这些需求更适合封装成指令,而不是在组件里写一堆 ref 和生命周期钩子。

自定义指令的本质

自定义指令本质上是一个对象,包含若干生命周期钩子函数,这些钩子会在绑定指令的元素上按特定顺序调用,并接收元素引用绑定值等参数。你可以像使用内置指令一样在模板中通过 v-xxx 来应用它们。

全局注册与局部注册

局部注册(仅在当前组件可用):

<script setup>
// 在 script setup 中,所有以 v 开头的驼峰变量都会被视为自定义指令
const vFocus = {
  mounted(el) {
    el.focus()
  }
}
</script>
<template>
  <input v-focus />
</template>

如果是选项式 API,则在组件的 directives 选项中注册:

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

全局注册(整个应用可用):

// main.js
const app = createApp(App)

app.directive('focus', {
  mounted(el) {
    el.focus()
  }
})
app.mount('#app')

全局注册的指令在任何组件内都能直接用 v-focus,但要注意命名冲突——尽量给指令起带前缀或明确含义的名字,避免覆盖内置指令或与其他库冲突。

生命周期钩子(Vue 3)

Vue 3 的自定义指令钩子与组件生命周期类似,但专门针对 DOM 绑定:

| 钩子函数 | 调用时机 | 常用操作 |
|---------|---------|--------|
| created | 在绑定元素的 attribute 或事件监听器应用之前调用 | 适合做一些初始设置,此时元素还未挂载 |
| beforeMount | 元素被插入到 DOM 之前调用 | - |
| mounted | 绑定元素的父组件及所有子节点都挂载完成后调用 | 最常用:操作 DOM、设置焦点、初始化第三方库 |
| beforeUpdate | 在元素自身更新之前调用,类似组件的 beforeUpdate | 适合在更新前移除已添加的监听器,但更推荐在 updated 中处理 |
| updated | 元素及其子节点更新完成后调用 | 基于新数据操作 DOM,但要避免无限更新循环 |
| beforeUnmount | 在元素被卸载之前调用 | 清理定时器、事件监听 |
| unmounted | 元素卸载后调用 | 最终清理 |

钩子参数:每个钩子函数都接收相同的参数集合:

  • el:指令绑定的 DOM 元素,可直接操作。
  • binding:一个对象,包含 value(传递给指令的值)、oldValuearg(参数,如 v-example:foo 中的 foo)、modifiers(修饰符对象)等。
  • vnode:当前元素的虚拟节点。
  • prevVnode:之前的虚拟节点(仅在 beforeUpdateupdated 中可用)。

简写形式:如果只想在 mountedupdated 时执行相同逻辑,可以直接传入一个函数:

app.directive('color', (el, binding) => {
  el.style.color = binding.value
})

这等同于 { mounted(el, binding) { ... }, updated(el, binding) { ... } }

典型应用场景

场景1:自动获取焦点

<script setup>
const vFocus = {
  mounted: (el) => el.focus()
}
</script>
<template>
  <input v-focus />
</template>

对话框中的第一个输入框自动聚焦,不再需要 ref 加生命周期调用。

场景2:点击外部关闭(常用于下拉菜单、模态框)

// 全局指令 v-click-outside
app.directive('click-outside', {
  mounted(el, binding) {
    el._clickOutside = (event) => {
      if (!(el === event.target || el.contains(event.target))) {
        binding.value(event) // 调用传递的回调函数
      }
    }
    document.addEventListener('click', el._clickOutside)
  },
  unmounted(el) {
    document.removeEventListener('click', el._clickOutside)
  }
})

使用:

<div v-click-outside="closeMenu">点击外部我会关闭</div>

场景3:按钮防抖点击

app.directive('debounce', {
  mounted(el, binding) {
    let timer
    el.addEventListener('click', (e) => {
      if (timer) clearTimeout(timer)
      timer = setTimeout(() => {
        binding.value(e) // 真正的处理函数
      }, binding.arg ? parseInt(binding.arg) : 300) // 可通过 v-debounce:500 指定延迟
    })
  }
})

使用:

<button v-debounce:500="handleSubmit">提交(500ms防抖)</button>

场景4:权限控制指令(v-permission)

// 根据用户权限决定元素是否展示
app.directive('permission', {
  mounted(el, binding) {
    const { value } = binding // 需要的权限标识
    const permissions = store.state.user.permissions // 假设从全局状态获取
    if (value && !permissions.includes(value)) {
      el.parentNode?.removeChild(el) // 无权限则移除元素
    }
  }
})

使用:

<button v-permission="'delete'">删除</button>  <!-- 用户无delete权限时此按钮不存在 -->

场景5:图片懒加载

app.directive('lazy', {
  mounted(el, binding) {
    const observer = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) {
        el.src = binding.value // 真实图片地址
        observer.unobserve(el)
      }
    })
    observer.observe(el)
  }
})

什么时候该用自定义指令

当逻辑的核心是操作 DOM,并且需要在多个组件中复用时,自定义指令比 Hooks 或 Mixin 更合适。指令直接附着在元素上,代码位置靠近使用处,意图一目了然。但要注意:如果逻辑包含大量与业务状态相关的计算,应将这部分抽成 Hooks,在指令中只保留 DOM 操作部分,保持职责单一。

注意事项

  • 指令中的函数如果使用了箭头函数,this 不指向组件实例;如果需要组件上下文,应使用 binding.instance
  • updated 钩子中修改 DOM 时要小心,可能引起更新循环,尽量只做“读取”或基于 binding.value 的“设置”。
  • 自定义指令不能像组件那样直接处理插槽或生成模板,它的领域是“对现有 DOM 元素的增强”。

自定义指令是 Vue 提供给开发者的“逃生舱”——当你需要越过声明式渲染直接触碰 DOM 时,它是最干净、最可维护的解决方案。