Vue2 与 Vue3 自定义指令详解:钩子变化、迁移指南与实战示例

Vue2 与 Vue3 自定义指令详解:钩子变化、迁移指南与实战示例

📌 自定义指令(Custom Directives)是对底层 DOM 进行底层操作的能力封装。当组件 / 组合式 API 解决不了「直接操作 DOM」这类需求时(如自动聚焦、权限控制、点击外部关闭、文本粘贴格式化等),指令是最自然的落点。


📖 前言

很多同学用 Vue 写业务时,习惯了 v-modelv-showv-if,却很少自己写自定义指令。一旦遇到「要在元素插入 DOM 后自动聚焦」「点击组件外部关闭弹层」「按权限动态隐藏元素」这类需求,才会意识到指令的价值。

本文用一张差异表 + 多个可运行示例,把 Vue2 与 Vue3 的自定义指令讲透,并给出从 Vue2 平滑迁移到 Vue3 的实战要点。建议收藏。


一、什么是自定义指令

指令本质上是「带 v- 前缀的可复用 DOM 行为」。Vue 内置了 v-modelv-showv-if 等;当内置指令不够用时,可以注册自己的指令。

一个指令对象包含若干「钩子函数」,Vue 在元素的不同生命周期阶段回调它们。

使用方式:

html 复制代码
<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"value2)。
    • oldValue:上一次的值,仅在 update / componentUpdated 中可用。
    • expression:字符串形式的绑定表达式(如 "1 + 1")。
    • arg:传给指令的参数(如 v-my-dir:fooarg"foo")。
    • modifiers:修饰符对象(如 v-my-dir.a.bmodifiers{ a: true, b: true })。
  • vnode:Vue 编译生成的虚拟节点。
  • oldVnode :上一个虚拟节点,仅在 update / componentUpdated 中可用。

⚠️ 除 el 外,其他参数都应是只读的,不要修改它们。

2.3 Vue2 注册方式

全局注册:

js 复制代码
// main.js
Vue.directive('focus', {
  inserted(el) {
    el.focus();
  }
});

局部注册:

js 复制代码
export default {
  directives: {
    focus: {
      inserted(el) {
        el.focus();
      }
    }
  }
};

2.4 Vue2 完整示例

示例 1:自动聚焦

js 复制代码
Vue.directive('focus', {
  // 元素插入 DOM 后聚焦(bind 阶段 el 还没进文档,所以用 inserted)
  inserted(el) {
    el.focus();
  }
});

示例 2:权限控制(函数简写形式)

如果指令在 bindupdate 时触发相同行为,可传入函数代替对象:

js 复制代码
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);
  }
});

函数写法等价于在 bindupdate 中调用同一个函数。

示例 3:动态参数 + 修饰符(拖拽)

html 复制代码
<div v-drag:horizontal.throttle="onDrag">拖我</div>
js 复制代码
Vue.directive('drag', {
  bind(el, binding) {
    const onlyX = binding.arg === 'horizontal';
    const throttle = binding.modifiers.throttle;
    let startX = 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 的 insertedVue3 的 beforeMount ≈ Vue2 的 bind。移植旧指令时最常用的替换就是这两对。

3.2 钩子参数

Vue3 的钩子同样接收 4 个参数,但 binding 结构略有调整:

  • el:DOM 元素(同 Vue2)。
  • binding :包含 instance新增 ,使用指令的组件实例)、valueoldValueargmodifiersdir(指令定义对象自身)。
  • vnode:虚拟节点。
  • prevVnode :上一个虚拟节点(updated 中可用,替代 Vue2 的 oldVnode)。

✅ Vue3 在 binding 中暴露了 instance(组件实例)和 dir(指令对象),便于在钩子间共享状态。

3.3 Vue3 注册方式

全局注册(使用 app 实例):

js 复制代码
// main.js
import { createApp } from 'vue';
import App from './App.vue';

const app = createApp(App);

app.directive('focus', {
  mounted(el) {
    el.focus();
  }
});

app.mount('#app');

局部注册(<script setup> + Composition API):

vue 复制代码
<script setup>
const vFocus = {
  mounted: (el) => el.focus()
};
</script>

<template>
  <input v-focus />
</template>

<script setup> 中,指令名必须以 v 开头的小驼峰变量名声明(如 vFocus 对应 v-focus)。

对象写法简写(函数形式,等价于 mounted + updated):

js 复制代码
app.directive('color', (el, binding) => {
  el.style.color = binding.value;
});

3.4 Vue3 完整示例

示例 1:自动聚焦

js 复制代码
app.directive('focus', {
  mounted(el) {
    el.focus();
  }
});

示例 2:点击外部关闭(Click Outside)

js 复制代码
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__;
  }
});
vue 复制代码
<template>
  <div v-click-outside="closeMenu" class="menu">菜单内容</div>
</template>

示例 3:利用 binding.instance 读取组件状态

js 复制代码
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 变量命名
oldVnodeprevVnode oldVnode prevVnode

五、迁移要点(Vue2 → Vue3)

  1. 钩子改名bindbeforeMountinsertedmountedcomponentUpdatedupdatedunbindunmounted

  2. 拆分 update :若原 update 里依赖「子节点已就绪」,迁移后应放到 updated;若依赖「自身更新前」,用 beforeUpdate

  3. 清理逻辑 :把事件监听、定时器、观察者等的清理统一放到 unmounted(原 unbind)。

  4. binding.instance 替代 hack :Vue2 常通过 vnode.context 拿组件实例,Vue3 直接用 binding.instance,更干净。

  5. TypeScript 类型 :Vue3 提供 DirectiveBindingDirective 等类型,推荐声明:

    ts 复制代码
    import type { 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 进阶笔记持续更新中。

相关推荐
AlienZHOU23 分钟前
AI Coding 时代下,我的技术面试实践分享
前端·后端·面试
Captaincc4 小时前
AI用量v0.1.11更新发布 新增 jusage doctor 诊断指令 托盘展示token 和余额 新增 AutoClaw 支持
前端·后端·vibecoding
计算机魔术师5 小时前
德国Wiki被黑后两周,OpenAI终于把模型失控的账本摊开了
前端
kyriewen5 小时前
我让 AI 当面试官面了我一轮:第 3 个追问我就卡住了(附 10 道追问清单)
前端·面试·ai编程
IT_陈寒6 小时前
Python的GIL把我坑惨了,多线程跑得比单线程还慢
前端·人工智能·后端
前端snow6 小时前
ai agent --- 多agent框架之图编排引擎-langgraph
前端
竹林8186 小时前
OmniPic Studio v3.2.1 核心技术架构与全平台发版解析文档
前端·浏览器
JamesZhang800786 小时前
页面内存只涨不跌? 一次泄漏排查, 牵出 WeakMap 的诞生
前端
Z小明6 小时前
第 6 章 组件进阶
前端·vue.js
江华森6 小时前
HTTP请求的完整过程详解:从DNS解析到TCP挥手的微秒级实战分析
前端