×

想学Vue3自定义指令却不知道从哪入手?

作者:Terry2025.06.23来源:Web前端之家浏览:54评论:0
关键词:Vue3自定义指令

VUE3

你可以把自定义指令理解成“强化版DOM操作工具”,Vue组件是封装UI和逻辑,但自定义指令更聚焦DOM行为的复用——比如让输入框自动聚焦、根据权限隐藏按钮、输入时自动格式化内容…这些和DOM强相关的逻辑,用指令封装后,在模板里加个v-xxx就能复用,比在组件里写重复代码清爽多了。

举个直观例子:登录页有个输入框,每次进入页面要自动聚焦,如果不用指令,得在组件onMounted里写inputRef.value.focus();但用自定义指令v-focus,只需要在输入框上加v-focus,所有需要自动聚焦的输入框都能复用这个逻辑,不用在每个组件里重复写DOM操作。

怎么创建第一个自定义指令?

全局指令局部指令两种注册方式,先从最简单的“自动聚焦”例子入手:

全局指令(全项目可用)

在Vue3的入口文件(比如main.js)里,用app.directive注册:

import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
// 注册全局指令v-focus
app.directive('focus', {
  // 钩子函数:DOM挂载后执行(此时才能操作DOM)
  mounted(el) {
    el.focus() // el是指令绑定的DOM元素
  }
})
app.mount('#app')

然后在任意组件的模板里用:

<template>
  <input v-focus placeholder="自动聚焦到我~" />
</template>

局部指令(只在当前组件可用)

如果指令只用在某个组件,就在组件的directives选项里定义:

<template>
  <input v-focus placeholder="局部指令的自动聚焦" />
</template>
<script setup>
// 局部注册v-focus
const directives = {
  focus: {
    mounted(el) {
      el.focus()
    }
  }
}
</script>

自定义指令的钩子函数有啥讲究?

Vue3给指令设计了7个钩子函数,对应DOM不同生命周期,理解它们的执行时机是写好指令的关键:

钩子函数执行时机常用场景
created指令绑定元素的JS对象创建后(DOM还没生成)少用,此时拿不到DOM
beforeMount指令绑定元素插入DOM前提前准备数据(用得少)
mounted指令绑定元素插入DOM后操作DOM(比如focus、加事件监听)
beforeUpdate组件更新前(数据变了,DOM还没更)提前处理数据(用得少)
updated组件更新后(数据和DOM都更新了)基于新DOM做操作(比如重新计算高度)
beforeUnmount组件卸载前准备清理工作(比如移除定时器)
unmounted组件卸载后彻底清理(比如移除事件监听)

举个实战例子:做一个“点击outside隐藏弹窗”的指令v-click-outside,需要在mounted时给文档加点击事件监听,在unmounted时移除监听(否则会内存泄漏):

app.directive('click-outside', {
  mounted(el, binding) {
    // binding.value是指令绑定的函数,比如v-click-outside="closeDialog"
    const handleClick = (e) => {
      if (!el.contains(e.target)) {
        binding.value() // 执行关闭弹窗的函数
      }
    }
    document.addEventListener('click', handleClick)
    // 把事件存到el上,方便unmounted时移除
    el._clickOutsideHandler = handleClick
  },
  unmounted(el) {
    document.removeEventListener('click', el._clickOutsideHandler)
  }
})

实战场景1:权限控制指令v-permission

项目里常见需求:不同角色看到的按钮不一样(删除”按钮只有管理员能看到),用v-permission指令实现根据权限动态控制DOM显示

需求分析

  • 指令接收“权限标识数组”,比如v-permission="['admin', 'editor']"

  • 页面加载时,检查用户是否有至少一个权限,没有就隐藏/删除按钮

实现步骤

  1. 假设用户权限存在Pinia或Vuex里,比如useUserStore().roles是当前用户角色数组。

  2. 注册全局指令v-permission:  

    import { useUserStore } from './stores/user'

app.directive('permission', { mounted(el, binding) { const userStore = useUserStore() const requiredRoles = binding.value // 指令绑定的权限数组,'admin'] // 检查用户是否有任一权限 const hasPermission = requiredRoles.some(role => userStore.roles.includes(role)) if (!hasPermission) { // 两种隐藏方式:移除DOM(彻底消失)或设为display:none(保留位置) el.parentNode?.removeChild(el) // 或者 el.style.display = 'none' } } })

3. 模板中使用:  
```vue
<template>
  <!-- 只有admin能看到这个按钮 -->
  <button v-permission="['admin']">删除文章</button>
</template>

实战场景2:防抖指令v-debounce

搜索框输入时,每次输入都发请求会浪费性能,用v-debounce让请求延迟触发(比如输入停止500ms后再发请求):

需求分析

  • 指令绑定在输入框,监听input事件

  • 每次输入时清除之前的定时器,重新计时,延迟执行请求函数

实现步骤

  1. 注册全局指令v-debounce,处理定时器逻辑:

    app.directive('debounce', {
    mounted(el, binding) {
     let timer = null
     const delay = 500 // 防抖延迟,也可以让指令接收参数控制
     el.addEventListener('input', () => {
       clearTimeout(timer)
       timer = setTimeout(() => {
         binding.value() // 执行指令绑定的函数,比如handleSearch
       }, delay)
     })
    }
    })
  2. 组件中使用(假设handleSearch是发请求的函数):

    <template>
    <input v-model="searchKey" v-debounce="handleSearch" placeholder="搜索..." />
    </template>

```

实战场景3:输入格式限制指令v-format

比如手机号输入时自动变成“3-4-4”格式(138 1234 5678),或只能输入数字,用v-format监听输入,实时格式化内容:

需求分析(以手机号为例)

  • 输入时,把连续数字按/(\d{3})(\d{4})(\d{4})/分割,加空格

  • 同时要更新v-model绑定的值(因为输入框的v-model是双向绑定,指令要触发input事件让Vue感知变化)

实现步骤

  1. 注册全局指令v-format

    app.directive('format', {
    mounted(el) {
     el.addEventListener('input', () => {
       // 去除所有非数字字符
       let value = el.value.replace(/\D/g, '') 
       // 格式化为3-4-4
       if (value.length > 3) {
         value = `${value.slice(0, 3)} ${value.slice(3, 7)} ${value.slice(7)}`
       } else if (value.length > 0) {
         value = value.slice(0, 3)
       }
       el.value = value
       // 触发input事件,让v-model更新绑定值
       el.dispatchEvent(new Event('input'))
     })
    }
    })
  2. 模板中使用:

    <template>
    <input v-model="phone" v-format placeholder="请输入手机号" />
    </template>

```

实战场景4:加载状态指令v-loading

按钮点击后显示“加载中”,防止重复点击,用v-loading控制按钮状态和样式:

需求分析

  • 指令接收一个布尔值,控制是否显示加载

  • 点击按钮时,若处于加载中则禁用按钮,显示加载文字

实现步骤

  1. 注册全局指令v-loading,结合样式和状态控制:

    app.directive('loading', {
    mounted(el, binding) {
     // 初始化:如果绑定值为true,直接设置加载状态
     if (binding.value) {
       el.disabled = true
       el.innerHTML = '加载中...'
     }
     // 监听值变化(用updated钩子,因为mounted只执行一次)
    },
    updated(el, binding) {
     if (binding.value) {
       el.disabled = true
       el.innerHTML = '加载中...'
     } else {
       el.disabled = false
       // 恢复原文字(假设按钮原文字存在data-original里)
       el.innerHTML = el.dataset.original || '提交'
     }
    }
    })
  2. 组件中使用(结合异步函数):

    <template>
    <button 
     v-loading="isLoading" 
     data-original="提交"
     @click="handleSubmit"
    >提交</button>
    </template>

```

自定义指令的进阶技巧:传参和修饰符

指令也能像组件一样传参数和加修饰符,让逻辑更灵活,比如v-permission:admin.lazy="true"

  • :admin参数(通过binding.arg获取)

  • .lazy修饰符(通过binding.modifiers.lazy获取)

例子:带参数的权限指令

需求:区分“查看”和“编辑”权限,用参数arg控制:

app.directive('permission', {
  mounted(el, binding) {
    const action = binding.arg // quot;view"或"edit"
    const requiredRoles = binding.value 
    // 假设权限格式是 { view: ['admin'], edit: ['editor'] }
    const userStore = useUserStore()
    const hasPermission = requiredRoles.some(role => userStore.roles[action].includes(role))
    if (!hasPermission) {
      el.parentNode?.removeChild(el)
    }
  }
})

模板中使用:

<!-- 只有admin能查看,editor能编辑 -->
<button v-permission:view="['admin']">查看</button>
<button v-permission:edit="['editor']">编辑</button>

写自定义指令要避开哪些坑?

  1. 内存泄漏:给DOM加了事件监听(比如addEventListener),一定要在unmounted钩子用removeEventListener移除,否则组件销毁后事件还在,会触发错误。

  2. DOM结构影响:用el.parentNode.removeChild(el)删除元素时,要考虑父组件布局会不会乱(比如按钮列表少了一个元素),如果只是“隐藏”,用el.style.display = 'none'更安全。

  3. 响应式数据更新:如果指令依赖的变量(比如用户权限、加载状态)变化了,要在updated钩子处理,因为mounted只执行一次,变量变化后不会重新触发。

  4. 指令复用冲突:如果多个组件用同一个指令,要确保指令内部状态不共享(比如定时器ID、事件处理函数),可以把状态存在el的自定义属性里(比如el._timer),避免不同元素互相影响。

自定义指令是Vue3里很“接地气”的工具——把重复的DOM操作逻辑封装成v-xxx,既让模板干净,又能复用,从自动聚焦、权限控制到防抖、输入格式化,这些场景练一遍,遇到类似需求直接套思路,多在项目里试几个指令,手感自然就来了~

您的支持是我们创作的动力!
温馨提示:本文作者系Terry ,经Web前端之家编辑修改或补充,转载请注明出处和本文链接:
https://jiangweishan.com/article/vue3jiahsdf235.html

网友评论文明上网理性发言 已有0人参与

发表评论: