Vue3 自定义指令的原理,以及应用

发布于:2025-05-08 ⋅ 阅读:(25) ⋅ 点赞:(0)


前言

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>

五、注意事项

  1. 组件首选:大部分逻辑推荐使用组件封装,只有在需要直接操作 DOM 的时候才用指令。
  2. 指令副作用清理:在 unmounted 中做清理(如移除监听器、取消定时器)。
  3. 性能考虑:不要在 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 模块,具体流程在组件渲染和更新阶段处理指令钩子(createdmountedupdated 等)。我们来深入讲解其源码执行机制。


✅ 自定义指令的底层实现原理概览

自定义指令在 Vue 编译和渲染过程中分两阶段处理:

  1. 编译阶段(仅在模板编译时):

    • v-xxx 语法解析为 withDirectives() 包裹的 VNode。
  2. 运行时渲染阶段

    • 调用 withDirectives(),将指令对象和绑定信息附加到 vnode。
    • mountElementpatchElement 时,调用各个指令生命周期钩子。

📦 相关核心函数 & 文件位置(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。


网站公告

今日签到

点亮在社区的每一天
去签到