一、啥是自定义指令凭啥要用它Vue 自带了一些指令比如v-model、v-if、v-show、v-for这些大家天天用。但有时候你想给元素加一个“自动聚焦”的行为可能会在onMounted里写input.focus()。如果有好几个页面都要这个功能你就得把那段代码复制来复制去又丑又容易漏。这时候自定义指令就派上用场了。你可以自己造一个v-focus然后往任何元素上一写它就自动聚焦一句废话都没有。一句话总结自定义指令就是让你把“操作 DOM 的逻辑”封装成一个个可复用的“标记”。二、先写一个最基础的局部自定义指令局部指令就是只在当前组件里能用定义在script setup里面。我们用自动聚焦来开篇。案例1页面打开输入框自动获得焦点vuetemplate div h3自动聚焦输入框/h3 !-- v-focus 是我们自定义的指令 名字前面有个 v但定义的时候只叫 focus -- input v-focus typetext placeholder我一出来就聚焦 / /div /template script setup // 定义一个局部自定义指令名字叫 focus // 注意定义时用小写 v 开头但要写成 vFocus 这种驼峰模板里用 v-focus const vFocus { // mounted 是钩子函数当元素被挂载到 DOM 上时调用 mounted(el) { // el 就是被绑定了这个指令的原生 DOM 元素这里就是 input el.focus() // 直接调用原生 focus 方法 } } /script拆开来讲指令对象里可以定义多个钩子函数最常用的就是mounted它在元素插入 DOM 后触发。mounted(el)里的el就是绑定了这个指令的原生 DOM 元素你可以对它做任何原生操作。为啥叫vFocus因为 Vue 会自动把模板里的v-focus和脚本里的vFocus关联起来。这是 Vue 的约定。三、全局自定义指令一次定义到处都能用局部指令只能在当前组件用很多实用功能我们希望在整个应用里都能直接用这就需要全局指令。在main.js里用app.directive注册。案例2全局自动聚焦指令main.jsjavascriptimport { createApp } from vue import App from ./App.vue const app createApp(App) // 注册全局指令名字叫 focus app.directive(focus, { mounted(el) { el.focus() } }) app.mount(#app)之后任何组件里都可以直接写input v-focus /不用再单独引入。四、指令的钩子函数和参数指令有好几个钩子跟组件的生命周期有点像但专门针对绑定了指令的这个元素。常用钩子created元素创建后但还没插入 DOM很少用。mounted元素插入 DOM 后最常用。updated组件更新导致 DOM 变化后。beforeUnmount元素被移除前。钩子函数可以拿到三个参数el绑定的元素本身。binding一个对象包含指令相关的信息比如value指令的值、arg参数、modifiers修饰符。vnode虚拟节点一般用不到。用一张表记住 binding属性说明示例v-demo:arg.modifiervaluebinding.value指令的值valuebinding.arg冒号后面的参数argbinding.modifiers修饰符对象{ modifier: true }我们用几个案例把这三个东西玩明白。五、带参数和值的指令案例3改变背景色指令 v-bg需求给元素加一个背景色颜色通过指令值传入。还可以用参数指定是背景色还是文字颜色。vuetemplate div !-- 使用 v-bg:backgroundlightblue 参数 background 表示改背景色 值 lightblue 是颜色 -- p v-bg:backgroundlightblue我的背景是浅蓝色/p !-- 参数 color 表示改文字颜色 -- p v-bg:colorred我的文字是红色/p !-- 不写参数默认改背景色 -- p v-bgyellow默认改背景色为黄色/p /div /template script setup const vBg { mounted(el, binding) { // binding.arg 是参数冒号后面的东西 // binding.value 是指令的值等号后面的东西 const arg binding.arg || background // 没传参数就默认改背景色 if (arg background) { el.style.backgroundColor binding.value } else if (arg color) { el.style.color binding.value } } } /script关键点binding.arg拿到参数background或colorbinding.value拿到颜色值。这样你就能用一个指令干两件事。六、带修饰符的指令修饰符就是点后面的东西比如v-on:click.prevent里的.prevent。案例4防抖点击指令 v-debounce需求有些按钮不能猛点比如支付按钮我们需要点击后一定时间内不能再次点击。用修饰符.lock控制是否“锁定”按钮。vuetemplate div !-- 使用 v-debounce.lockhandlePay .lock 修饰符表示点击后锁定按钮 1 秒防止重复点击 值 handlePay 是点击后要执行的回调函数 -- button v-debounce.lockhandlePay支付锁定1秒/button !-- 不加 .lock 修饰符就是普通点击不做防抖 -- button v-debouncehandleNormal普通按钮/button /div /template script setup import { ref } from vue const vDebounce { mounted(el, binding) { // 防抖时间写死 1 秒也可以做成指令参数 const delay 1000 let timer null // 检查修饰符里有没有 lock const needLock binding.modifiers.lock // 给元素绑定点击事件 el.addEventListener(click, () { if (needLock timer) { // 如果有 lock 修饰符且定时器还在说明还没解禁直接返回 return } // 执行传进来的回调函数 if (typeof binding.value function) { binding.value() } if (needLock) { // 按钮禁用样式 el.disabled true // 设置定时器1 秒后解禁 timer setTimeout(() { el.disabled false timer null }, delay) } }) } } function handlePay() { console.log(支付请求已发送按钮已锁定1秒) // 实际项目这里发请求 } function handleNormal() { console.log(普通点击) } /script解析binding.modifiers.lock是true时我们用setTimeout锁定按钮。binding.value是传进来的函数我们直接调用它。这个指令把“防抖”逻辑完全封装了起来模板里看起来非常干净。七、指令也能接收对象类型的值上面传的都是简单值其实binding.value可以是任意类型包括对象。这样就能传多个参数。案例5权限控制指令 v-permission需求根据用户权限决定某个元素是否显示。我们假设当前用户角色是editor只有角色匹配时才显示元素。vuetemplate div !-- 传一个对象给指令包含角色列表和是否移除 roles: [admin, editor] 表示这两个角色可见 remove: true 表示没有权限时直接移除DOM否则只是隐藏 -- button v-permission{ roles: [admin], remove: false } 只有管理员可见隐藏方式 /button button v-permission{ roles: [admin], remove: true } 只有管理员可见移除方式 /button !-- 当前用户是 editor所以上面两个按钮都不可见下面这个可见 -- button v-permission{ roles: [admin, editor] } 管理员和编辑可见 /button /div /template script setup import { ref } from vue // 假设当前用户角色是 editor const currentRole ref(editor) const vPermission { mounted(el, binding) { const { roles [], remove false } binding.value || {} // 检查当前角色是否在允许的角色列表里 const hasPermission roles.includes(currentRole.value) if (!hasPermission) { if (remove) { // 直接移除 DOM 元素 el.parentNode?.removeChild(el) } else { // 或者隐藏 el.style.display none } } } } /script说明binding.value拿到了整个对象{ roles: [...], remove: true }。我们可以解构出来然后根据逻辑决定显示、隐藏还是移除。八、综合实战案例图片懒加载指令 v-lazy这是前端里非常实用的一个功能页面上的图片先不加载等它滚到可视区域了再加载节省带宽和提升性能。vuetemplate div h3往下滚图片会懒加载/h3 !-- 占位高度让页面能滚 -- div styleheight: 800px; background: #f5f5f5; text-align: center; line-height: 800px; 请向下滚动 /div !-- v-lazy 指令值传图片的真实地址 初始时先显示一张占位图可以是透明的或 loading 图 -- img v-lazyhttps://picsum.photos/id/1/400/300 alt懒加载图片1 / br / img v-lazyhttps://picsum.photos/id/2/400/300 alt懒加载图片2 / br / img v-lazyhttps://picsum.photos/id/3/400/300 alt懒加载图片3 / /div /template script setup const vLazy { mounted(el, binding) { // 先设置占位图一张灰色的小图或 loading 图 el.src data:image/svgxml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIPGRlZnMPGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiB4MT0iMCUiIHkxPSIwJSIgeDI9IjEwMCUiIHkyPSIxMDAlIj48c3RvcCBvZmZzZXQ9IjAlIiBzdG9wLWNvbG9yPSIjZGRkIiAvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2VlZSIgLz48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48cmVjdCB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIGZpbGw9InVybCgjZ3JhZCkiIC8PC9zdmc // 真实图片地址存起来 const realSrc binding.value // 创建 IntersectionObserver 监听元素是否进入可视区域 const observer new IntersectionObserver((entries) { entries.forEach(entry { if (entry.isIntersecting) { // 进入可视区域了加载真实图片 el.src realSrc // 加载完就停止监听 observer.unobserve(el) } }) }, { rootMargin: 0px 0px 100px 0px // 提前 100px 就开始加载 }) // 开始观察这个元素 observer.observe(el) // 组件卸载时也清理 observer这里简化处理一般会存下来在 unmounted 里断开 el._observer observer }, unmounted(el) { // 清理观察器 if (el._observer) { el._observer.disconnect() } } } /script核心原理利用浏览器原生的IntersectionObserverAPI监听元素是否进入视口。进入时把占位图的src换成真实地址。同时做了卸载时的清理工作。这个指令可以直接用到任何项目里非常实用。九、总结一下自定义指令的写法步骤说明1. 定义const vXxx { mounted(el, binding) { ... } }2. 使用div v-xxx值/div3. 参数通过binding.arg拿4. 修饰符通过binding.modifiers拿5. 值通过binding.value拿可以是任意类型局部注册在script setup里定义vXxx对象就行模板自动识别。全局注册在main.js里app.directive(xxx, { ... })。十、什么时候用组件什么时候用指令这个问题很多新手会纠结简单给个判断标准如果需要一段 HTML 模板 逻辑 样式用组件。如果只是对某个 DOM 元素做一点操作改样式、加事件、调焦点用自定义指令更轻量。比如权限隐藏用组件包装也可以但如果只是加个判断一个指令就搞定不用多套一层 div。