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-permission="'admin'"/><p v-highlight="'yellow'">高亮文本</p>二、Vue2 自定义指令
2.1 钩子函数(Hook Functions)
Vue2 指令对象可包含以下 5 个钩子:
| 钩子 | 调用时机 |
|---|---|
bind | 指令第一次绑定到元素时调用,只调用一次。此时 DOM 还未插入父节点(无法获取parentNode)。 |
inserted | 被绑定元素插入父节点时调用(仅保证父节点存在,不保证已在document中)。 |
update | 所在组件的 VNode 更新时调用,可能发生在其子 VNode 更新之前。指令的值可能改变,也可能没变。 |
componentUpdated | 指令所在组件的 VNode 及其子 VNode 全部更新后调用。 |
unbind | 指令与元素解绑时调用,只调用一次。 |
2.2 钩子参数
每个钩子接收相同的 4 个参数:
el:指令绑定的真实 DOM 元素,可直接操作。binding:一个对象,包含以下属性:name:指令名(不含v-前缀)。value:指令的绑定值(例如v-my-dir="1 + 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 })。
vnode:Vue 编译生成的虚拟节点。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 userRole=store.getters.role;// 假设从全局状态读取 const required=binding.value;// 指令绑定值,如'admin'if(!required.includes(userRole)){el.parentNode&&el.parentNode.removeChild(el);}});函数写法等价于在
bind和update中调用同一个函数。
示例 3:动态参数 + 修饰符(拖拽)
<div v-drag:horizontal.throttle="onDrag">拖我</div>Vue.directive('drag',{bind(el, binding){const onlyX=binding.arg==='horizontal';const throttle=binding.modifiers.throttle;letstartX=0, startY=0;const onMove=(e)=>{if(onlyX){el.style.left=e.clientX - startX +'px';}else{el.style.left=e.clientX - startX +'px';el.style.top=e.clientY - startY +'px';}};const onDown=(e)=>{startX=e.clientX - el.offsetLeft;startY=e.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 钩子 | 说明 |
|---|---|---|
bind | beforeMount | 指令绑定到元素,但还没挂载到 DOM。 |
inserted | mounted | 元素被插入父 DOM 后调用。 |
| — | beforeUpdate | 新增:元素自身 VNode 更新前(Vue2 没有对应独立钩子)。 |
update | —(移除) | 被beforeUpdate+updated取代,语义更清晰。 |
componentUpdated | updated | 组件及子组件 VNode 全部更新后。 |
unbind | unmounted | 指令与元素解绑、元素卸载时调用。 |
关键点:Vue3 的
mounted≈ Vue2 的inserted;Vue3 的beforeMount≈ Vue2 的bind。移植旧指令时最常用的替换就是这两对。
3.2 钩子参数
Vue3 的钩子同样接收 4 个参数,但binding结构略有调整:
el:DOM 元素(同 Vue2)。binding:包含instance(新增,使用指令的组件实例)、value、oldValue、arg、modifiers、dir(指令定义对象自身)。vnode:虚拟节点。prevVnode:上一个虚拟节点(updated中可用,替代 Vue2 的oldVnode)。
✅ Vue3 在
binding中暴露了instance(组件实例)和dir(指令对象),便于在钩子间共享状态。
3.3 Vue3 注册方式
全局注册(使用app实例):
// main.jsimport{createApp}from'vue';importApp from'./App.vue';const app=createApp(App);app.directive('focus',{mounted(el){el.focus();}});app.mount('#app');局部注册(<script setup>+ Composition API):
<script setup>const vFocus={mounted:(el)=>el.focus()};</script><template><input v-focus /></template>在
<script setup>中,指令名必须以v开头的小驼峰变量名声明(如vFocus对应v-focus)。
对象写法简写(函数形式,等价于mounted+updated):
app.directive('color',(el, binding)=>{el.style.color=binding.value;});3.4 Vue3 完整示例
示例 1:自动聚焦
app.directive('focus',{mounted(el){el.focus();}});示例 2:点击外部关闭(Click Outside)
app.directive('click-outside',{mounted(el, binding){// 在 mounted 注册监听,binding.value 为回调函数 el.__clickOutside__=(event)=>{if(!(el===event.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__;}});<template><div v-click-outside="closeMenu"class="menu">菜单内容</div></template>示例 3:利用binding.instance读取组件状态
app.directive('role',{mounted(el, binding){const vm=binding.instance;// 当前组件实例 const userRole=vm.userRole;// 直接读组件响应式数据if(!binding.value.includes(userRole)){el.style.display='none';}}, updated(el, binding){const vm=binding.instance;el.style.display=binding.value.includes(vm.userRole)?'':'none';}});四、Vue2 ↔ Vue3 核心差异速查
| 对比项 | Vue2 | Vue3 |
|---|---|---|
| 全局注册 API | Vue.directive() | app.directive()(需createApp实例) |
钩子bind | ✅ | 改名beforeMount |
钩子inserted | ✅ | 改名mounted |
钩子update | ✅ | 移除,拆为beforeUpdate+updated |
钩子componentUpdated | ✅ | 改名updated |
钩子unbind | ✅ | 改名unmounted |
新增beforeUpdate | ❌ | ✅ |
binding.instance | ❌ | ✅(可访问组件实例) |
binding.dir | ❌ | ✅(可访问指令对象自身) |
<script setup>局部指令 | 不支持(无此语法) | 支持,vXxx变量命名 |
oldVnode→prevVnode | oldVnode | prevVnode |
五、迁移要点(Vue2 → Vue3)
钩子改名:
bind→beforeMount、inserted→mounted、componentUpdated→updated、unbind→unmounted。拆分
update:若原update里依赖「子节点已就绪」,迁移后应放到updated;若依赖「自身更新前」,用beforeUpdate。清理逻辑:把事件监听、定时器、观察者等的清理统一放到
unmounted(原unbind)。binding.instance替代 hack:Vue2 常通过vnode.context拿组件实例,Vue3 直接用binding.instance,更干净。TypeScript 类型:Vue3 提供
DirectiveBinding、Directive等类型,推荐声明:importtype{Directive, DirectiveBinding}from'vue';const vFocus: Directive<HTMLElement, 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 进阶笔记持续更新中。