vue2 和 vue3自定义指令有什么区别,都是怎么实现和使用一个指令
Vue2 和 Vue3自定义指令(Custom Directive)整体思想一样:
直接操作 DOM 的一种扩展机制,通常用于权限控制、焦点、拖拽、懒加载等。
但 API 设计、生命周期、实现方式有明显变化。
4个层面:
1️⃣ Vue2 vs Vue3 指令生命周期区别
2️⃣ Vue2 按钮权限指令实现
3️⃣ Vue3 按钮权限指令实现
4️⃣ Vue3 指令底层设计变化
一、Vue2 vs Vue3 指令生命周期区别
Vue2 指令钩子
Vue2 指令有 5个生命周期
| 钩子 | 说明 |
|---|---|
| bind | 指令第一次绑定到元素 |
| inserted | 元素插入 DOM |
| update | VNode 更新 |
| componentUpdated | 组件更新完成 |
| unbind | 解绑 |
示例
Vue.directive('focus',{bind(el){},inserted(el){},update(el){},componentUpdated(el){},unbind(el){}})Vue3 指令生命周期
Vue3 完全重写了指令生命周期,名字和组件生命周期保持一致。
| Vue2 | Vue3 |
|---|---|
| bind | beforeMount |
| inserted | mounted |
| update | updated |
| componentUpdated | updated |
| unbind | unmounted |
示例
app.directive('focus',{beforeMount(el){},mounted(el){},updated(el){},unmounted(el){}})二、Vue2 按钮权限指令实现
企业中最常见的自定义指令就是:
按钮权限控制
v-permission例如
<buttonv-permission="'user:add'">新增</button>如果没有权限:按钮直接删除
Vue2 指令实现
- 定义指令
importstorefrom'@/store'Vue.directive('permission',{inserted(el,binding){const{value}=bindingconstpermissions=store.state.user.permissionsif(value&&valueinstanceofArray){consthasPermission=permissions.some(p=>value.includes(p))if(!hasPermission){el.parentNode.removeChild(el)}}else{thrownewError('权限指令需要数组')}}})- 使用指令
<buttonv-permission="['user:add']">新增用户</button>binding 参数结构
Vue2:
binding={name:'permission',value:['user:add'],oldValue:undefined,expression:"['user:add']",arg:undefined,modifiers:{}}三、Vue3 按钮权限指令实现
Vue3 写法更简洁。
- 创建指令
src/directives/permission.ts
importtype{Directive}from'vue'import{useUserStore}from'@/store/user'exportconstpermission:Directive={mounted(el,binding){const{value}=bindingconstuserStore=useUserStore()constpermissions=userStore.permissionsconsthasPermission=permissions.some(p=>value.includes(p))if(!hasPermission){el.parentNode?.removeChild(el)}}}- 注册指令
main.ts
import{createApp}from'vue'import{permission}from'@/directives/permission'constapp=createApp(App)app.directive('permission',permission)app.mount('#app')- 使用指令
<buttonv-permission="['user:add']">新增用户</button>四、Vue3 指令底层设计变化
Vue3 指令其实是 VNode patch 阶段执行。
关键源码在:
runtime-core/directives.ts核心函数:
invokeDirectiveHook简化源码:
exportfunctioninvokeDirectiveHook(vnode,prevVNode,instance,name){constbindings=vnode.dirsfor(leti=0;i<bindings.length;i++){constbinding=bindings[i]consthook=binding.dir[name]if(hook){hook(vnode.el,binding,vnode,prevVNode)}}}执行流程:
template ↓ 编译成 render ↓ VNode 上挂 dirs ↓ patch 阶段 ↓ invokeDirectiveHook ↓ 执行 mounted / updatedVNode结构:
{type:'button',props:{},dirs:[{dir:permission,value:['user:add']}]}五、Vue2 vs Vue3 指令实现差异
| 区别 | Vue2 | Vue3 |
|---|---|---|
| 注册 | Vue.directive | app.directive |
| 生命周期 | bind inserted update | beforeMount mounted updated |
| 调用时机 | patch | patch |
| binding 参数 | 复杂 | 更简单 |
| 类型支持 | 无 | TS Directive 类型 |
| 底层实现 | directive.js | runtime-core/directives.ts |
六、 Vue3 指令系统真正的运行路径
源码执行链路
template ↓ 编译 render ↓ withDirectives() ↓ VNode.dirs ↓ patch() ↓ invokeDirectiveHook() ↓ 执行 mounted / updated / unmounted七、模板里的指令是怎么变成 VNode 的
模板
<buttonv-permission="['user:add']">新增</button>编译后的 render 函数(简化版)
import{withDirectives,createVNode}from"vue"returnwithDirectives(createVNode("button",null,"新增"),[[permission,['user:add']]])关键函数:
withDirectives()函数的作用是:把指令挂到 VNode 上
八、withDirectives 源码
源码位置:
packages/runtime-core/src/directives.ts核心代码(简化)
exportfunctionwithDirectives(vnode,directives){constbindings=vnode.dirs||(vnode.dirs=[])for(leti=0;i<directives.length;i++){let[dir,value,arg,modifiers]=directives[i]bindings.push({dir,value,arg,modifiers})}returnvnode}执行完之后:
VNode 结构会变成:
{type:"button",props:null,children:"新增",dirs:[{dir:permission,value:['user:add'],arg:undefined,modifiers:{}}]}重点:VNode.dirs这里存放所有指令
九、patch 阶段如何执行指令
Vue3 DOM 渲染核心:renderer.ts
当元素创建时:mountElement()
核心流程:mountElement(vnode, container)
简化代码:
constmountElement=(vnode,container)=>{constel=vnode.el=document.createElement(vnode.type)// propspatchProps(el)// childrenmountChildren()// 指令 mountedif(vnode.dirs){invokeDirectiveHook(vnode,null,parentComponent,'mounted')}}所以:mounted是在DOM 创建完成之后执行。
十、invokeDirectiveHook(核心函数)
源码:
runtime-core/directives.ts核心代码:
exportfunctioninvokeDirectiveHook(vnode,prevVNode,instance,name){constbindings=vnode.dirsfor(leti=0;i<bindings.length;i++){constbinding=bindings[i]consthook=binding.dir[name]if(hook){hook(vnode.el,binding,vnode,prevVNode)}}}执行逻辑:
遍历 vnode.dirs ↓ 找到对应生命周期 ↓ 执行 mounted / updated调用指令:
permission.mounted(el,binding)十一、binding 参数真正结构
当 VNode 更新:patchElement()
源码:
if(dirs){invokeDirectiveHook(n2,n1,parentComponent,'updated')}执行顺序:
patch props patch children ↓ directive updated所以:updated 一定在 DOM 更新后执行
十二、指令 unmounted 执行时机
组件卸载:unmount()
源码:
if(vnode.dirs){invokeDirectiveHook(vnode,null,instance,'unmounted')}十三、Vue2 指令底层 vs Vue3 指令底层
Vue2 指令是在:patch.js执行updateDirectives()
源码:
functionupdateDirectives(oldVnode,vnode){constdirs=normalizeDirectives()for(keyindirs){callHook(dir,'bind')}}逻辑非常复杂
Vue3重写原因:
1️⃣ 生命周期混乱
2️⃣ diff逻辑复杂
3️⃣ 指令和组件生命周期不一致
Vue3改进:
统一生命周期 统一调用入口 VNode直接挂 dirs后话:
为什么 Vue3 要有 withDirectives
Vue3 是 函数式 VNode 创建:
h('button')没有 template 的情况下:
h('button', {}, '新增')只能:
withDirectives()
withDirectives(h('button'),[[permission,['user:add']]])所以:withDirectives 是 render 层 API