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 进阶笔记持续更新中。

相关推荐
问商十三载1 小时前
大模型GEO内容更新频率:3个节奏规律提收录权重,零成本提30%引用率附更新计划表
前端·人工智能·机器学习
yingyima1 小时前
sed 命令速查手册:从一次午夜事故说起
前端
七月丶1 小时前
线上出 Bug,你还在让用户"清一下缓存"?我用一个插件让每个请求自报版本
前端·javascript·面试
飘尘1 小时前
让AI能够预演现实的世界模型,到底是什么?
前端·人工智能
不简说1 小时前
JS 代码技巧 vol.3 — 10 个防抖节流深挖,小白到老炮的踩坑路
前端·javascript·面试
小智社群2 小时前
VR漫游视频完整版
前端·javascript·音视频
小程故事多_803 小时前
拆解SOP+ReAct对话Agent,告别硬编码,解锁智能对话的柔性迭代能力
前端·人工智能·react.js·前端框架
lzjava20243 小时前
LangChain Prompt提示词工程
前端·langchain·prompt
nicole bai3 小时前
vue 自定义模块内容
前端·javascript·vue.js