你可以把自定义指令理解成“强化版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']"
页面加载时,检查用户是否有至少一个权限,没有就隐藏/删除按钮
实现步骤
假设用户权限存在Pinia或Vuex里,比如
useUserStore().roles
是当前用户角色数组。注册全局指令
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
事件每次输入时清除之前的定时器,重新计时,延迟执行请求函数
实现步骤
注册全局指令
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) }) } })
组件中使用(假设
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感知变化)
实现步骤
注册全局指令
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')) }) } })
模板中使用:
<template> <input v-model="phone" v-format placeholder="请输入手机号" /> </template>
```
实战场景4:加载状态指令v-loading
按钮点击后显示“加载中”,防止重复点击,用v-loading
控制按钮状态和样式:
需求分析
指令接收一个布尔值,控制是否显示加载
点击按钮时,若处于加载中则禁用按钮,显示加载文字
实现步骤
注册全局指令
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 || '提交' } } })
组件中使用(结合异步函数):
<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>
写自定义指令要避开哪些坑?
内存泄漏:给DOM加了事件监听(比如
addEventListener
),一定要在unmounted
钩子用removeEventListener
移除,否则组件销毁后事件还在,会触发错误。DOM结构影响:用
el.parentNode.removeChild(el)
删除元素时,要考虑父组件布局会不会乱(比如按钮列表少了一个元素),如果只是“隐藏”,用el.style.display = 'none'
更安全。响应式数据更新:如果指令依赖的变量(比如用户权限、加载状态)变化了,要在
updated
钩子处理,因为mounted
只执行一次,变量变化后不会重新触发。指令复用冲突:如果多个组件用同一个指令,要确保指令内部状态不共享(比如定时器ID、事件处理函数),可以把状态存在
el
的自定义属性里(比如el._timer
),避免不同元素互相影响。
自定义指令是Vue3里很“接地气”的工具——把重复的DOM操作逻辑封装成v-xxx
,既让模板干净,又能复用,从自动聚焦、权限控制到防抖、输入格式化,这些场景练一遍,遇到类似需求直接套思路,多在项目里试几个指令,手感自然就来了~
网友评论文明上网理性发言 已有0人参与
发表评论: