文章目录
前言
Vue 3 自定义指令(Custom Directives)是对 DOM 元素的底层操作进行封装的一种机制,适用于处理组件模板中难以通过普通属性或事件完成的逻辑,例如拖拽、自动聚焦、懒加载、权限控制等场景。
一、原理说明
Vue 3 中,自定义指令是通过组合式 API directive
函数注册的,本质上是对某个 DOM 元素生命周期钩子的扩展封装。底层由 Directive
对象描述,具备以下钩子函数:
const myDirective = {
created(el, binding, vnode, prevVnode) {}, // 元素创建后调用
beforeMount(el, binding, vnode, prevVnode) {},// 挂载前
mounted(el, binding, vnode, prevVnode) {}, // 挂载后
beforeUpdate(el, binding, vnode, prevVnode) {},
updated(el, binding, vnode, prevVnode) {},
beforeUnmount(el, binding, vnode, prevVnode) {},
unmounted(el, binding, vnode, prevVnode) {}
}
el
: 被绑定的 DOM 元素binding
: 包含传递给指令的值、修饰符等vnode
: 虚拟节点prevVnode
: 之前的虚拟节点
二、注册与使用
1. 全局注册
const app = createApp(App)
app.directive('focus', {
mounted(el) {
el.focus()
}
})
2. 局部注册
export default {
directives: {
focus: {
mounted(el) {
el.focus()
}
}
}
}
3. 使用方式
<input v-focus />
三、典型应用场景
场景 | 描述 |
---|---|
自动聚焦 | 页面加载后 input 自动聚焦 |
拖拽指令 | 实现元素的拖动行为 |
权限控制 | 根据权限隐藏/禁用某些 DOM 元素 |
懒加载 | 图片进入可视区域再加载 |
节流防抖 | 限制用户频繁点击按钮的行为 |
动态样式绑定 | 更底层地控制样式/类名,比如动画、transition 控制 |
四、案例:权限控制指令
app.directive('permission', {
mounted(el, binding) {
const userPermissions = ['read']
if (!userPermissions.includes(binding.value)) {
el.parentNode?.removeChild(el)
}
}
})
<button v-permission="'admin'">仅管理员可见</button>
五、注意事项
- 组件首选:大部分逻辑推荐使用组件封装,只有在需要直接操作 DOM 的时候才用指令。
- 指令副作用清理:在
unmounted
中做清理(如移除监听器、取消定时器)。 - 性能考虑:不要在
updated
中重复进行复杂操作。
v-draggable
我们以 拖拽指令 v-draggable
为例,讲解一个完整的 Vue 3 自定义指令应用。
✅ 目标效果:
使任意元素可拖动,按住鼠标左键拖动到任意位置。
🧩 1. 自定义指令定义
// directives/draggable.ts
import type { Directive } from 'vue'
const draggable: Directive = {
mounted(el) {
el.style.position = 'absolute'
el.style.cursor = 'move'
let offsetX = 0
let offsetY = 0
const parent = el.offsetParent || document.body
const parentRect = parent.getBoundingClientRect()
const onMouseDown = (e: MouseEvent) => {
offsetX = e.clientX - el.offsetLeft
offsetY = e.clientY - el.offsetTop
document.addEventListener('mousemove', onMouseMove)
document.addEventListener('mouseup', onMouseUp)
}
const onMouseMove = (e: MouseEvent) => {
let left = e.clientX - offsetX
let top = e.clientY - offsetY
// ✅ 限制在父容器内
const maxLeft = parent.clientWidth - el.offsetWidth
const maxTop = parent.clientHeight - el.offsetHeight
left = Math.max(0, Math.min(left, maxLeft))
top = Math.max(0, Math.min(top, maxTop))
el.style.left = `${left}px`
el.style.top = `${top}px`
}
const onMouseUp = () => {
// ✅ 自动吸附到左右边缘
const left = el.offsetLeft
const center = parent.clientWidth / 2
el.style.left = left < center ? '0px' : `${parent.clientWidth - el.offsetWidth}px`
document.removeEventListener('mousemove', onMouseMove)
document.removeEventListener('mouseup', onMouseUp)
}
el.addEventListener('mousedown', onMouseDown)
;(el as any)._onMouseDown = onMouseDown
},
unmounted(el) {
el.removeEventListener('mousedown', (el as any)._onMouseDown)
}
}
export default draggable
🧱 2. 在项目中注册
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import draggable from './directives/draggable'
const app = createApp(App)
app.directive('draggable', draggable)
app.mount('#app')
🧪 3. 使用示例
<!-- App.vue -->
<template>
<div
v-draggable
style="width: 100px; height: 100px; background: #42b983;"
>
拖我
</div>
</template>
📝 说明:
el.style.position = 'absolute'
:让元素能够移动- 鼠标按下时,记录初始偏移;
- 鼠标移动时更新
left/top
; - 鼠标松开时清除事件监听,避免内存泄漏。
功能 | 实现方式 |
---|---|
拖动 | mousedown + mousemove + mouseup |
边界限制 | 利用 parent.clientWidth /clientHeight 计算边界 |
自动吸附左右 | 释放鼠标后判断是否靠近左/右边,设置 left 值 |
Vue 3 中自定义指令的源码实现,核心位于其 runtime-core 模块,具体流程在组件渲染和更新阶段处理指令钩子(created
、mounted
、updated
等)。我们来深入讲解其源码执行机制。
✅ 自定义指令的底层实现原理概览
自定义指令在 Vue 编译和渲染过程中分两阶段处理:
编译阶段(仅在模板编译时):
- 将
v-xxx
语法解析为withDirectives()
包裹的 VNode。
- 将
运行时渲染阶段:
- 调用
withDirectives()
,将指令对象和绑定信息附加到 vnode。 - 在
mountElement
和patchElement
时,调用各个指令生命周期钩子。
- 调用
📦 相关核心函数 & 文件位置(Vue 3)
功能 | 函数名 | 文件位置 |
---|---|---|
将指令附加到 vnode | withDirectives |
runtime-core/directives.ts |
执行指令生命周期钩子 | invokeDirectiveHook |
runtime-core/renderer.ts |
调用时机:挂载/更新 | mountElement , patchElement |
runtime-core/renderer.ts |
🧩 源码分析:流程详解
① 编译模板为虚拟节点时
模板语法:
<div v-focus="true" />
会被编译为:
withDirectives(h('div'), [
[focusDirective, true]
])
② withDirectives()
函数
export function withDirectives(vnode: VNode, directives: DirectiveArguments): VNode {
vnode.dirs = directives
return vnode
}
这里会把指令数组保存在 vnode 上,供后续 mount 或 patch 时处理。
③ mountElement()
阶段:执行 mounted 钩子
if (dirs) {
invokeDirectiveHook(vnode, null, parentComponent, 'beforeMount')
}
patchProps(...) // 设置属性等
if (dirs) {
queuePostRenderEffect(() => {
invokeDirectiveHook(vnode, null, parentComponent, 'mounted')
}, parentSuspense)
}
④ invokeDirectiveHook()
执行钩子
function invokeDirectiveHook(
vnode: VNode,
prevVNode: VNode | null,
instance: ComponentInternalInstance,
name: DirectiveHookName
) {
const dirs = vnode.dirs!
for (let i = 0; i < dirs.length; i++) {
const dir = dirs[i]
const hook = dir[0][name]
if (hook) {
callWithAsyncErrorHandling(
hook,
instance,
ErrorCodes.DIRECTIVE_HOOK,
[
vnode.el,
dir[1], // binding.value
vnode,
prevVNode
]
)
}
}
}
👆这个函数就是最终 真正调用你写的 mounted()
、updated()
等函数 的地方。
🧠 自定义指令对象结构(本质)
你注册的指令最终是以下结构:
[
directive: {
mounted,
updated,
...
},
value, // binding.value
arg, // binding.arg
modifiers, // binding.modifiers
instance // component instance
]
✅ 总结:Vue 3 自定义指令运行机制
阶段 | 关键点 |
---|---|
编译阶段 | v-xxx 被转成 withDirectives() 包装 |
渲染阶段 | vnode.dirs 被附加到 VNode |
挂载时 | invokeDirectiveHook(..., 'mounted') 被调用 |
更新时 | invokeDirectiveHook(..., 'updated') 被调用 |
模板编译:
Vue 编译 v-xxx 为 withDirectives(h(…), [directive]) 结构。
withDirectives:
将指令数组 dirs 附加到虚拟节点 vnode 上,供后续处理。
mountElement:
创建 DOM 元素时,如果发现 vnode.dirs 存在,就调度钩子(如 mounted)。
invokeDirectiveHook:
遍历所有绑定指令,调用你注册的生命周期函数(如 mounted()、updated())。
执行你写的钩子函数:
如 el.focus()、拖拽逻辑等,操作真实 DOM。