Vue2 与 Vue3 自定义指令详解:钩子变化、迁移指南与实战示例

📅 2026/7/18 20:52:18
Vue2 与 Vue3 自定义指令详解:钩子变化、迁移指南与实战示例
Vue2 与 Vue3 自定义指令详解钩子变化、迁移指南与实战示例 自定义指令Custom Directives是对底层 DOM 进行底层操作的能力封装。当组件 / 组合式 API 解决不了「直接操作 DOM」这类需求时如自动聚焦、权限控制、点击外部关闭、文本粘贴格式化等指令是最自然的落点。 前言很多同学用 Vue 写业务时习惯了v-model、v-show、v-if却很少自己写自定义指令。一旦遇到「要在元素插入 DOM 后自动聚焦」「点击组件外部关闭弹层」「按权限动态隐藏元素」这类需求才会意识到指令的价值。本文用一张差异表 多个可运行示例把 Vue2 与 Vue3 的自定义指令讲透并给出从 Vue2 平滑迁移到 Vue3 的实战要点。建议收藏。一、什么是自定义指令指令本质上是「带v-前缀的可复用 DOM 行为」。Vue 内置了v-model、v-show、v-if等当内置指令不够用时可以注册自己的指令。一个指令对象包含若干「钩子函数」Vue 在元素的不同生命周期阶段回调它们。使用方式input v-focus /div v-permissionadmin/p v-highlightyellow高亮文本/p二、Vue2 自定义指令2.1 钩子函数Hook FunctionsVue2 指令对象可包含以下 5 个钩子钩子调用时机bind指令第一次绑定到元素时调用只调用一次。此时 DOM 还未插入父节点无法获取parentNode。inserted被绑定元素插入父节点时调用仅保证父节点存在不保证已在document中。update所在组件的 VNode 更新时调用可能发生在其子 VNode 更新之前。指令的值可能改变也可能没变。componentUpdated指令所在组件的 VNode 及其子 VNode 全部更新后调用。unbind指令与元素解绑时调用只调用一次。2.2 钩子参数每个钩子接收相同的 4 个参数el指令绑定的真实 DOM 元素可直接操作。binding一个对象包含以下属性name指令名不含v-前缀。value指令的绑定值例如v-my-dir1 1中value为2。oldValue上一次的值仅在update/componentUpdated中可用。expression字符串形式的绑定表达式如1 1。arg传给指令的参数如v-my-dir:foo中arg为foo。modifiers修饰符对象如v-my-dir.a.b中modifiers为{ a: true, b: true }。vnodeVue 编译生成的虚拟节点。oldVnode上一个虚拟节点仅在update/componentUpdated中可用。⚠️ 除el外其他参数都应是只读的不要修改它们。2.3 Vue2 注册方式全局注册// main.js Vue.directive(focus,{inserted(el){el.focus();}});局部注册exportdefault{directives:{focus:{inserted(el){el.focus();}}}};2.4 Vue2 完整示例示例 1自动聚焦Vue.directive(focus,{// 元素插入 DOM 后聚焦bind 阶段 el 还没进文档所以用 inserted inserted(el){el.focus();}});示例 2权限控制函数简写形式如果指令在bind和update时触发相同行为可传入函数代替对象Vue.directive(permission,function(el, binding){const userRolestore.getters.role;// 假设从全局状态读取 const requiredbinding.value;// 指令绑定值如adminif(!required.includes(userRole)){el.parentNodeel.parentNode.removeChild(el);}});函数写法等价于在bind和update中调用同一个函数。示例 3动态参数 修饰符拖拽div v-drag:horizontal.throttleonDrag拖我/divVue.directive(drag,{bind(el, binding){const onlyXbinding.arghorizontal;const throttlebinding.modifiers.throttle;letstartX0, startY0;const onMove(e){if(onlyX){el.style.lefte.clientX - startX px;}else{el.style.lefte.clientX - startX px;el.style.tope.clientY - startY px;}};const onDown(e){startXe.clientX - el.offsetLeft;startYe.clientY - el.offsetTop;document.addEventListener(mousemove, onMove);};const onUp()document.removeEventListener(mousemove, onMove);el.addEventListener(mousedown, onDown);el.addEventListener(mouseup, onUp);// 注意真实场景应在 unbind 中清理事件监听避免内存泄漏}});三、Vue3 自定义指令3.1 钩子函数变化重大差异Vue3 重命名并调整了指令钩子与组件生命周期对齐Vue2 钩子Vue3 钩子说明bindbeforeMount指令绑定到元素但还没挂载到 DOM。insertedmounted元素被插入父 DOM 后调用。—beforeUpdate新增元素自身 VNode 更新前Vue2 没有对应独立钩子。update—移除被beforeUpdateupdated取代语义更清晰。componentUpdatedupdated组件及子组件 VNode 全部更新后。unbindunmounted指令与元素解绑、元素卸载时调用。关键点Vue3 的mounted≈ Vue2 的insertedVue3 的beforeMount≈ Vue2 的bind。移植旧指令时最常用的替换就是这两对。3.2 钩子参数Vue3 的钩子同样接收 4 个参数但binding结构略有调整elDOM 元素同 Vue2。binding包含instance新增使用指令的组件实例、value、oldValue、arg、modifiers、dir指令定义对象自身。vnode虚拟节点。prevVnode上一个虚拟节点updated中可用替代 Vue2 的oldVnode。✅ Vue3 在binding中暴露了instance组件实例和dir指令对象便于在钩子间共享状态。3.3 Vue3 注册方式全局注册使用app实例// main.jsimport{createApp}fromvue;importApp from./App.vue;const appcreateApp(App);app.directive(focus,{mounted(el){el.focus();}});app.mount(#app);局部注册script setup Composition APIscript setupconst vFocus{mounted:(el)el.focus()};/scripttemplateinput v-focus //template在script setup中指令名必须以v开头的小驼峰变量名声明如vFocus对应v-focus。对象写法简写函数形式等价于mountedupdatedapp.directive(color,(el, binding){el.style.colorbinding.value;});3.4 Vue3 完整示例示例 1自动聚焦app.directive(focus,{mounted(el){el.focus();}});示例 2点击外部关闭Click Outsideapp.directive(click-outside,{mounted(el, binding){// 在 mounted 注册监听binding.value 为回调函数 el.__clickOutside__(event){if(!(elevent.target||el.contains(event.target))){binding.value(event);}};document.addEventListener(click, el.__clickOutside__,true);}, unmounted(el){document.removeEventListener(click, el.__clickOutside__,true);delete el.__clickOutside__;}});templatediv v-click-outsidecloseMenuclassmenu菜单内容/div/template示例 3利用binding.instance读取组件状态app.directive(role,{mounted(el, binding){const vmbinding.instance;// 当前组件实例 const userRolevm.userRole;// 直接读组件响应式数据if(!binding.value.includes(userRole)){el.style.displaynone;}}, updated(el, binding){const vmbinding.instance;el.style.displaybinding.value.includes(vm.userRole)?:none;}});四、Vue2 ↔ Vue3 核心差异速查对比项Vue2Vue3全局注册 APIVue.directive()app.directive()需createApp实例钩子bind✅改名beforeMount钩子inserted✅改名mounted钩子update✅移除拆为beforeUpdateupdated钩子componentUpdated✅改名updated钩子unbind✅改名unmounted新增beforeUpdate❌✅binding.instance❌✅可访问组件实例binding.dir❌✅可访问指令对象自身script setup局部指令不支持无此语法支持vXxx变量命名oldVnode→prevVnodeoldVnodeprevVnode五、迁移要点Vue2 → Vue3钩子改名bind→beforeMount、inserted→mounted、componentUpdated→updated、unbind→unmounted。拆分update若原update里依赖「子节点已就绪」迁移后应放到updated若依赖「自身更新前」用beforeUpdate。清理逻辑把事件监听、定时器、观察者等的清理统一放到unmounted原unbind。binding.instance替代 hackVue2 常通过vnode.context拿组件实例Vue3 直接用binding.instance更干净。TypeScript 类型Vue3 提供DirectiveBinding、Directive等类型推荐声明importtype{Directive, DirectiveBinding}fromvue;const vFocus: DirectiveHTMLElement, void{mounted(el){el.focus();}};六、最佳实践✅能用组件 / 组合式函数解决的优先不用指令。指令是「最后手段」因为直接操作 DOM 会绕过 Vue 的响应式系统。✅务必在unmounted/unbind中清理副作用事件监听、ResizeObserver、定时器、IntersectionObserver等否则内存泄漏。✅不要在指令里修改binding对象它是只读快照。✅ 复杂指令可用binding.dir存共享状态或把状态挂在el.__xxx__上注意命名冲突建议加前缀。✅ 指令值尽量传「函数 / 对象」而非命令式字符串便于类型推导与测试。七、小结自定义指令 封装底层 DOM 行为的复用单元。Vue3 把指令钩子对齐组件生命周期全部带Mount语义并新增beforeUpdate、暴露binding.instance。迁移核心就是四个钩子改名 update拆分 清理逻辑归位。指令应作为「直接操作 DOM」场景的兜底方案注意副作用清理。如果这篇文章帮你理清了 Vue 指令的迁移思路欢迎点赞 ❤️、收藏 ⭐、关注 并在评论区聊聊你在项目中用指令解决过什么奇葩需求本文同步整理自个人知识库更多 Vue 进阶笔记持续更新中。