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 | 解绑 |
示例
js
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 |
示例
js
app.directive('focus', {
beforeMount(el) {},
mounted(el) {},
updated(el) {},
unmounted(el) {}
})
二、Vue2 按钮权限指令实现
企业中最常见的自定义指令就是:
按钮权限控制
v-permission
例如
html
<button v-permission="'user:add'">新增</button>
如果没有权限:按钮直接删除
Vue2 指令实现
- 定义指令
js
import store from '@/store'
Vue.directive('permission', {
inserted(el, binding) {
const { value } = binding
const permissions = store.state.user.permissions
if (value && value instanceof Array) {
const hasPermission = permissions.some(
p => value.includes(p)
)
if (!hasPermission) {
el.parentNode.removeChild(el)
}
} else {
throw new Error('权限指令需要数组')
}
}
})
- 使用指令
html
<button v-permission="['user:add']">
新增用户
</button>
binding 参数结构
Vue2:
js
binding = {
name: 'permission',
value: ['user:add'],
oldValue: undefined,
expression: "['user:add']",
arg: undefined,
modifiers: {}
}
三、Vue3 按钮权限指令实现
Vue3 写法更简洁。
- 创建指令
src/directives/permission.ts
ts
import type { Directive } from 'vue'
import { useUserStore } from '@/store/user'
export const permission: Directive = {
mounted(el, binding) {
const { value } = binding
const userStore = useUserStore()
const permissions = userStore.permissions
const hasPermission = permissions.some(
p => value.includes(p)
)
if (!hasPermission) {
el.parentNode?.removeChild(el)
}
}
}
- 注册指令
main.ts
ts
import { createApp } from 'vue'
import { permission } from '@/directives/permission'
const app = createApp(App)
app.directive('permission', permission)
app.mount('#app')
- 使用指令
html
<button v-permission="['user:add']">
新增用户
</button>
四、Vue3 指令底层设计变化
Vue3 指令其实是 VNode patch 阶段执行。
关键源码在:
bash
runtime-core/directives.ts
核心函数:
invokeDirectiveHook
简化源码:
ts
export function invokeDirectiveHook(
vnode,
prevVNode,
instance,
name
) {
const bindings = vnode.dirs
for (let i = 0; i < bindings.length; i++) {
const binding = bindings[i]
const hook = binding.dir[name]
if (hook) {
hook(vnode.el, binding, vnode, prevVNode)
}
}
}
执行流程:
arduino
template
↓
编译成 render
↓
VNode 上挂 dirs
↓
patch 阶段
↓
invokeDirectiveHook
↓
执行 mounted / updated
VNode结构:
js
{
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 指令系统真正的运行路径
源码执行链路
scss
template
↓
编译 render
↓
withDirectives()
↓
VNode.dirs
↓
patch()
↓
invokeDirectiveHook()
↓
执行 mounted / updated / unmounted
七、模板里的指令是怎么变成 VNode 的
模板
html
<button v-permission="['user:add']">
新增
</button>
编译后的 render 函数(简化版)
js
import { withDirectives, createVNode } from "vue"
return withDirectives(
createVNode("button", null, "新增"),
[
[permission, ['user:add']]
]
)
关键函数:
scss
withDirectives()
函数的作用是:把指令挂到 VNode 上
八、withDirectives 源码
源码位置:
bash
packages/runtime-core/src/directives.ts
核心代码(简化)
ts
export function withDirectives(vnode, directives) {
const bindings = vnode.dirs || (vnode.dirs = [])
for (let i = 0; i < directives.length; i++) {
let [dir, value, arg, modifiers] = directives[i]
bindings.push({
dir,
value,
arg,
modifiers
})
}
return vnode
}
执行完之后:
VNode 结构会变成:
js
{
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)
简化代码:
ts
const mountElement = (vnode, container) => {
const el = vnode.el = document.createElement(vnode.type)
// props
patchProps(el)
// children
mountChildren()
// 指令 mounted
if (vnode.dirs) {
invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
}
}
所以:mounted 是在 DOM 创建完成之后执行。
十、invokeDirectiveHook(核心函数)
源码:
bash
runtime-core/directives.ts
核心代码:
ts
export function invokeDirectiveHook(
vnode,
prevVNode,
instance,
name
) {
const bindings = vnode.dirs
for (let i = 0; i < bindings.length; i++) {
const binding = bindings[i]
const hook = binding.dir[name]
if (hook) {
hook(
vnode.el,
binding,
vnode,
prevVNode
)
}
}
}
执行逻辑:
遍历 vnode.dirs
↓
找到对应生命周期
↓
执行 mounted / updated
调用指令:
js
permission.mounted(el, binding)
十一、binding 参数真正结构
当 VNode 更新:patchElement()
源码:
ts
if (dirs) {
invokeDirectiveHook(
n2,
n1,
parentComponent,
'updated'
)
}
执行顺序:
patch props
patch children
↓
directive updated
所以:updated 一定在 DOM 更新后执行
十二、指令 unmounted 执行时机
组件卸载:unmount()
源码:
ts
if (vnode.dirs) {
invokeDirectiveHook(vnode, null, instance, 'unmounted')
}
十三、Vue2 指令底层 vs Vue3 指令底层
Vue2 指令是在:patch.js 执行 updateDirectives()
源码:
js
function updateDirectives(oldVnode, vnode) {
const dirs = normalizeDirectives()
for (key in dirs) {
callHook(dir, 'bind')
}
}
逻辑非常复杂
Vue3重写原因:
1️⃣ 生命周期混乱 2️⃣ diff逻辑复杂 3️⃣ 指令和组件生命周期不一致
Vue3改进:
bash
统一生命周期
统一调用入口
VNode直接挂 dirs
后话:
为什么 Vue3 要有 withDirectives
Vue3 是 函数式 VNode 创建:
js
h('button')
没有 template 的情况下:
less
h('button', {}, '新增')
只能:
withDirectives()
js
withDirectives(
h('button'),
[[permission, ['user:add']]]
)
所以:withDirectives 是 render 层 API