Vue 自定义指令完全指南(含 Vue2/Vue3 对比 + 完整 Demo)

官方自定义指令使用文档

https://cn.vuejs.org/guide/reusability/custom-directives.html#custom-directiveshttps://cn.vuejs.org/guide/reusability/custom-directives.html#custom-directives

1. vue3 指令钩子

javascript 复制代码
const myDirective = {
    // 周期函数:钩子函数
    // created: 指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
    // beforeMount: 指令所在元素插入父节点前调用。在这里可以进行初始化操作,例如添加事件监听器或设置初始状态。
    // mounted: 指令所在元素插入父节点后调用。在这里可以进行 DOM 操作,例如添加事件监听器或设置初始状态。
    // beforeUpdate: 绑定元素的父组件更新前调用。在这里可以进行更新前的操作,例如保存当前状态或更新依赖的属性。
    // updated: 指令所在组件的 VNode 及其子 VNode 全部更新后调用。在这里可以进行更新后的操作,例如根据新值更新 DOM。
    // beforeUnmount: 绑定元素的父组件卸载前调用。在这里可以进行卸载前的操作,例如移除事件监听器或清理资源。
    // unmounted: 指令所在元素从 DOM 中移除后调用。在这里可以进行卸载后的操作,例如清理资源或重置状态。
    // 参数说明:
    // el:指令绑定到的元素。这可以用于直接操作 DOM。
    // binding:一个对象,包含以下属性。
    // value:传递给指令的值。例如在 v-my-directive="1 + 1" 中,值是 2。
    // oldValue:之前的值,仅在 beforeUpdate 和 updated 中可用。无论值是否更改,它都可用。
    // arg:传递给指令的参数 (如果有的话)。例如在 v-my-directive:foo 中,参数是 "foo"。
    // modifiers:一个包含修饰符的对象 (如果有的话)。例如在 v-my-directive.foo.bar 中,修饰符对象是 { foo: true, bar: true }。
    // instance:使用该指令的组件实例。
    // dir:指令的定义对象。
    // vnode:代表绑定元素的底层 VNode。
    // prevVnode:代表之前的渲染中指令所绑定元素的 VNode。仅在 beforeUpdate 和 updated 钩子中可用。

    // ====分割线====
    // 在绑定元素的 attribute 前
    // 或事件监听器应用前调用
    created(el, binding, vnode) {
        // 下面会介绍各个参数的细节
    },
    // 在元素被插入到 DOM 前调用
    beforeMount(el, binding, vnode) {},
    // 在绑定元素的父组件
    // 及他自己的所有子节点都挂载完成后调用
    mounted(el, binding, vnode) {},
    // 绑定元素的父组件更新前调用
    beforeUpdate(el, binding, vnode, prevVnode) {},
    // 在绑定元素的父组件
    // 及他自己的所有子节点都更新后调用
    updated(el, binding, vnode, prevVnode) {},
    // 绑定元素的父组件卸载前调用
    beforeUnmount(el, binding, vnode) {},
    // 绑定元素的父组件卸载后调用
    unmounted(el, binding, vnode) {},
}

2. vue2钩子函数

javascript 复制代码
Vue.directive('demo', {
    // 周期函数:钩子函数
    // bind: 只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
    // inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
    // update: 所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新(详细的钩子函数参数见下)。
    // componentUpdated: 指令所在组件的 VNode 及其子 VNode 全部更新后调用。
    // unbind: 只调用一次,指令与元素解绑时调用。
    // 指令所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新(详细的钩子函数参数见下)。
    // 参数说明:
    // el 指令绑定的元素。
    // binding 一个对象,包含以下 property:
    // - name: 指令名,不包括 v- 前缀。
    // - value: 指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2。
    // - oldValue: 指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。无论值是否改变都可用。
    // - expression: 绑定值的字符串形式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"。
    // - arg: 传给指令的参数。例如 v-my-directive:foo 中,参数为 "foo"。
    // - modifiers: 一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }。
    bind: function (el, binding, vnode) {
        var s = JSON.stringify
    },
    inserted: function (el, binding, vnode, oldVnode) {
        // 对比更新前后的值,忽略不必要的模板更新。
    },
    update: function (el, binding, vnode, oldVnode) {
        // 通常你只应该聚焦于更新的部分。对比更新前后的值,忽略不必要的模板更新。
    },
    // componentUpdated: 指令所在组件的 VNode 及其子 VNode 全部更新后调用。
    componentUpdated: function (el, binding, vnode, oldVnode) {
        // 对比更新前后的值,忽略不必要的模板更新。
    },
    // unbind: 只调用一次,指令与元素解绑时调用。
    unbind: function (el, binding, vnode, oldVnode) {
        // 清理指令绑定的元素上的事件监听器或其他资源。
    },
})

Vue 自定义指令是 Vue 提供的核心扩展能力之一,用于封装可复用的 DOM 操作逻辑(如表单校验、权限控制、元素动画、输入框失焦等场景)。它允许开发者在不侵入组件逻辑的前提下,对 DOM 元素进行底层操作,弥补了 Vue 内置指令(v-if/v-for/v-bind 等)的灵活性不足。​

本文将从「基础用法→核心钩子→实战场景→Vue2/Vue3 对比→高级技巧」逐步拆解,结合 Vue2 和 Vue3 双版本 Demo 示例,帮助你彻底掌握自定义指令的使用。​

一、自定义指令的核心概念​

1. 定义与分类​

  • 全局指令:在 Vue 实例 / 应用全局注册,所有组件均可使用(如 v-focus)。
  • 局部指令:在单个组件内注册,仅当前组件可用。
  • 核心作用:封装 DOM 相关的重复逻辑,避免在组件 mounted/updated 等钩子中写大量重复代码。

2. Vue2 vs Vue3 核心钩子函数(生命周期)​

自定义指令的核心差异集中在钩子名称和执行时机,以下是对比表格(重点关注常用钩子):​

|----------------|-------------------|----------------|------------------------------------|
| 功能场景​ | Vue2 钩子函数​ | Vue3 钩子函数​ | 触发时机说明​ |
| 元素挂载后初始化​ | inserted​ | mounted​ | 元素已挂载到 DOM 树,适合执行聚焦、事件绑定等初始化操作​ |
| 指令绑定到元素(未挂载)​ | bind​ | created​ | 指令绑定到元素,但元素未挂载到 DOM,无法操作 DOM 节点​ |
| 组件更新前(DOM 未更)​ | update​ | beforeUpdate​ | 组件数据更新,DOM 尚未重新渲染​ |
| 组件更新后(DOM 已更)​ | componentUpdated​ | updated​ | 组件数据更新,DOM 已重新渲染,适合同步更新 DOM 状态​ |
| 元素卸载前​ | -​ | beforeUnmount​ | Vue3 新增,元素卸载前执行(如清理定时器前的准备)​ |
| 元素卸载后(清理)​ | unbind​ | unmounted​ | 元素从 DOM 卸载,必须在此清理事件监听、定时器等,避免内存泄漏​ |

关键参数(Vue2/Vue3 通用):​

  • el:指令绑定的真实 DOM 元素,可直接操作(如 el.focus()、el.style.color = 'red')。
  • binding:指令的详细信息对象,核心属性:
  • value:指令绑定的值(如 v-my-directive="100" 中,value 为 100)。
  • arg:指令参数(如 v-my-directive:click 中,arg 为 click)。
  • modifiers:指令修饰符(如 v-my-directive.prevent 中,modifiers 为 { prevent: true })。
  • vnode:虚拟节点(Vue 编译后的节点描述),Vue3 新增 prevVnode(旧虚拟节点)参数。

二、Vue2 vs Vue3 基础用法对比(Demo 实战)​

场景 1:全局指令 v-focus(输入框自动聚焦)​

Vue2 实现(全局指令)​

javascript 复制代码
// main.js(Vue2)
import Vue from 'vue'
import App from './App.vue'

// 注册全局指令:v-focus
Vue.directive('focus', {
  // Vue2 用 inserted 钩子(元素挂载到 DOM 后触发)
  inserted(el) {
    // el 是绑定指令的 input 元素
    el.focus() 
  }
})

new Vue({
  render: h => h(App)
}).$mount('#app')

Vue3 实现(全局指令)​​

javascript 复制代码
// main.js(Vue3)
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

// 注册全局指令:v-focus
app.directive('focus', {
  // Vue3 用 mounted 钩子(替代 Vue2 的 inserted)
  mounted(el) {
    el.focus() 
  }
})

app.mount('#app')

共同使用示例(组件内)​

javascript 复制代码
<!-- Vue2 和 Vue3 模板用法完全一致 -->
<template>
  <input v-focus type="text" placeholder="自动聚焦的输入框">
</template>

场景 2:局部指令 v-color(动态设置文字颜色)​

Vue2 实现(局部指令)​

javascript 复制代码
<!-- Vue2 组件:ColorDemo.vue -->
<template>
  <div v-color="colorValue" style="font-size: 20px;">
    Vue2 局部指令:文字颜色动态变化
  </div>
  <button @click="colorValue = 'red'">变红</button>
  <button @click="colorValue = 'blue'">变蓝</button>
</template>

<script>
export default {
  // Vue2 局部指令注册:directives 选项
  directives: {
    color: {
      // 绑定初始化时执行(元素未挂载)
      bind(el, binding) {
        el.style.color = binding.value
      },
      // 组件更新时执行(数据变化后)
      update(el, binding) {
        el.style.color = binding.value
      }
    }
  },
  data() {
    return {
      colorValue: 'green' // 初始颜色
    }
  }
}
</script>

Vue3 实现(局部指令,<script setup> 语法)​

javascript 复制代码
<!-- Vue3 组件:ColorDemo.vue -->
<template>
  <div v-color="colorValue" style="font-size: 20px;">
    Vue3 局部指令:文字颜色动态变化
  </div>
  <button @click="colorValue = 'red'">变红</button>
  <button @click="colorValue = 'blue'">变蓝</button>
</template>

<script setup>
import { ref } from 'vue'

// Vue3 <script setup> 中:局部指令需命名为 vXXX(驼峰)
const vColor = {
  // 元素挂载时执行(替代 Vue2 的 bind)
  mounted(el, binding) {
    el.style.color = binding.value
  },
  // 组件更新时执行(替代 Vue2 的 update)
  updated(el, binding) {
    el.style.color = binding.value
  }
}

// 响应式颜色值(Vue3 用 ref 替代 Vue2 的 data)
const colorValue = ref('green')
</script>

场景 3:函数式指令(简化写法)​

当指令逻辑仅需在「初始化 + 更新」触发且逻辑一致时,Vue2 和 Vue3 均支持函数式简化写法:​

Vue2 函数式指令​​

javascript 复制代码
<!-- Vue2 函数式指令 -->
<template>
  <div v-simple-color="colorValue">Vue2 函数式指令</div>
</template>

<script>
export default {
  directives: {
    // 函数式指令:自动在 bind 和 update 执行
    simpleColor(el, binding) {
      el.style.color = binding.value
    }
  },
  data() {
    return { colorValue: 'orange' }
  }
}
</script>

Vue3 函数式指令​

javascript 复制代码
<!-- Vue3 函数式指令 -->
<template>
  <div v-simple-color="colorValue">Vue3 函数式指令</div>
</template>

<script setup>
import { ref } from 'vue'

// 函数式指令:自动在 mounted 和 updated 执行
const vSimpleColor = (el, binding) => {
  el.style.color = binding.value
}

const colorValue = ref('orange')
</script>

三、Vue2 vs Vue3 实战场景对比(复杂指令)​

场景:v-blur-on-outside(点击空白处失焦,兼容 iOS)​

Vue2 实现(复杂指令:事件监听 + 清理)​

javascript 复制代码
// Vue2 全局指令:directives/vBlurOnOutside.js
export default {
  // 元素挂载到 DOM 后绑定事件
  inserted(el) {
    let isTouchingInput = false

    // 触摸开始:判断是否点击输入框
    const handleTouchStart = (e) => {
      isTouchingInput = el.contains(e.target)
    }

    // 触摸结束:非输入框则失焦
    const handleTouchEnd = () => {
      if (!isTouchingInput && document.activeElement === el) {
        setTimeout(() => el.blur(), 50) // 适配 iOS 滚动
      }
      isTouchingInput = false
    }

    // 绑定文档事件
    document.addEventListener('touchstart', handleTouchStart)
    document.addEventListener('touchend', handleTouchEnd)

    // 存储清理函数到 el 上(Vue2 无 prevVnode,需手动挂载)
    el._unbindBlur = () => {
      document.removeEventListener('touchstart', handleTouchStart)
      document.removeEventListener('touchend', handleTouchEnd)
    }
  },
  // 元素卸载时清理(Vue2 用 unbind 钩子)
  unbind(el) {
    el._unbindBlur() // 执行清理
  }
}

// 全局注册(main.js)
import Vue from 'vue'
import vBlurOnOutside from './directives/vBlurOnOutside'
Vue.directive('blur-on-outside', vBlurOnOutside)

Vue3 实现(复杂指令:新增 prevVnode 参数)​

javascript 复制代码
// Vue3 全局指令:directives/vBlurOnOutside.js
export default {
  // 元素挂载后绑定事件(Vue3 用 mounted)
  mounted(el) {
    let isTouchingInput = false

    const handleTouchStart = (e) => {
      isTouchingInput = el.contains(e.target)
    }

    const handleTouchEnd = () => {
      if (!isTouchingInput && document.activeElement === el) {
        setTimeout(() => el.blur(), 50)
      }
      isTouchingInput = false
    }

    document.addEventListener('touchstart', handleTouchStart)
    document.addEventListener('touchend', handleTouchEnd)

    // 存储清理函数
    el._unbindBlur = () => {
      document.removeEventListener('touchstart', handleTouchStart)
      document.removeEventListener('touchend', handleTouchEnd)
    }
  },
  // 元素卸载时清理(Vue3 用 unmounted,替代 Vue2 的 unbind)
  unmounted(el, binding, vnode, prevVnode) {
    // Vue3 新增 prevVnode:可对比新旧虚拟节点(此处无需使用)
    el._unbindBlur()
  }
}

// 全局注册(main.js)
import { createApp } from 'vue'
import App from './App.vue'
import vBlurOnOutside from './directives/vBlurOnOutside'
const app = createApp(App)
app.directive('blur-on-outside', vBlurOnOutside)
app.mount('#app')

共同使用示例​

javascript 复制代码
<!-- Vue2 和 Vue3 模板用法一致 -->
<template>
  <input 
    v-blur-on-outside 
    type="text" 
    placeholder="点击空白处失焦(iOS 兼容)"
    style="padding: 10px; font-size: 16px;"
  >
</template>

四、Vue2 与 Vue3 自定义指令核心差异总结​

|---------------------------|----------------------------------|----------------------------------------------|
| 对比维度​ | Vue2 特性​ | Vue3 特性​ |
| 全局注册方式​ | Vue.directive('name', options)​ | app.directive('name', options)​ |
| 局部注册方式(普通语法)​ | directives 组件选项​ | directives 组件选项(与 Vue2 一致)​ |
| 局部注册方式(<script setup>)​ | 不支持(无 <script setup>)​ | 支持,指令命名需为 vXXX(驼峰),模板用 v-xxx​ |
| 核心钩子映射​ | bind → 初始化(未挂载)​ | created → 初始化(未挂载)​ |
| ​ | inserted → 元素挂载后​ | mounted → 元素挂载后(替代 inserted)​ |
| ​ | update → 组件更新(DOM 未更)​ | beforeUpdate → 组件更新(DOM 未更)​ |
| ​ | componentUpdated → 组件更新(DOM 已更)​ | updated → 组件更新(DOM 已更)​ |
| ​ | unbind → 元素卸载清理​ | unmounted → 元素卸载清理(替代 unbind)​ |
| 新增钩子​ | 无​ | beforeUnmount(元素卸载前)​ |
| 钩子参数​ | el, binding, vnode​ | el, binding, vnode, prevVnode(新增 prevVnode)​ |
| 函数式指令执行时机​ | bind + update​ | mounted + updated​ |

关键差异强调

  1. 钩子名称变化:Vue3 统一了钩子命名(与组件生命周期钩子对齐,如 mounted/unmounted),更易理解。
  2. <script setup> 支持:Vue3 的 <script setup> 语法对局部指令有特殊命名要求(驼峰 vXXX),而 Vue2 无此语法。
  3. 新增参数:Vue3 钩子新增 prevVnode 参数,可对比新旧虚拟节点(适合复杂更新逻辑)。
  4. 全局注册上下文:Vue2 全局注册依赖 Vue 构造函数,Vue3 依赖 createApp 创建的应用实例(更符合模块化)。

五、其他实战场景(Vue2/Vue3 通用 Demo)​

具体的全局注册和局部注册,可参考场景2,均已列举!

场景 1:权限控制指令 v-permission​

javascript 复制代码
// Vue3 通用指令逻辑
export default {
  mounted(el, binding) {
    const requiredPermissions = binding.value || [] // 所需权限
    const userPermissions = ['editor'] // 模拟用户权限(实际从存储获取)
    
    // 无权限则隐藏元素
    if (!requiredPermissions.some(p => userPermissions.includes(p))) {
      el.style.display = 'none'
    }
  }
}

// Vue3 注册:app.directive('permission', 上述对象)
javascript 复制代码
// vue2 directives/permission.js(单独抽离,方便全局引入)
export default {
  // Vue2 用 inserted 钩子(对应 Vue3 的 mounted)
  inserted(el, binding) {
    const requiredPermissions = binding.value || []; // 所需权限(指令传入值)
    const userPermissions = ['editor']; // 模拟用户权限(实际从 Vuex/本地存储获取)
    
    // 无权限则隐藏元素
    if (!requiredPermissions.some(p => userPermissions.includes(p))) {
      el.style.display = 'none';
    }
  }
};


// Vue2 注册:Vue.directive('permission', 上述对象)

使用示例(Vue2/Vue3 一致):​

javascript 复制代码
<template>
  <button v-permission="['admin']">管理员操作</button>
  <button v-permission="['admin', 'editor']">编辑操作</button>
</template>

场景 2:防抖指令 v-debounce​

局部注册vue3

javascript 复制代码
<template>
  <!-- 点击防抖(500ms延迟) -->
  <button v-debounce:500="handleClick">点击触发</button>
  <!-- 输入防抖(默认300ms) -->
  <input v-debounce.input="handleInput" placeholder="输入测试">
</template>

<script setup>
// 局部注册 v-debounce 指令
const vDebounce = {
  mounted(el, binding) {
    const fn = binding.value;
    const delay = binding.arg || 300;
    let timer = null;

    el._debounceHandler = () => {
      clearTimeout(timer);
      timer = setTimeout(fn, delay);
    };

    const eventType = binding.modifiers.input ? 'input' : 'click';
    el.addEventListener(eventType, el._debounceHandler);
    el._debounceEventType = eventType; // 存储事件类型
  },
  unmounted(el) {
    clearTimeout(el.timer);
    el.removeEventListener(el._debounceEventType, el._debounceHandler);
    el._debounceHandler = el._debounceEventType = null;
  }
};

// 测试回调函数
const handleClick = () => console.log('点击防抖触发');
const handleInput = (e) => console.log('输入防抖触发:', e.target.value);
</script>

全局注册vue3

javascript 复制代码
// directives/debounce.js
export default {
  mounted(el, binding) {
    const fn = binding.value;
    const delay = binding.arg || 300;
    let timer = null;

    el._debounceHandler = () => {
      clearTimeout(timer);
      timer = setTimeout(fn, delay);
    };

    const eventType = binding.modifiers.input ? 'input' : 'click';
    el.addEventListener(eventType, el._debounceHandler);
    el._debounceEventType = eventType;
  },
  unmounted(el) {
    clearTimeout(el.timer);
    el.removeEventListener(el._debounceEventType, el._debounceHandler);
    el._debounceHandler = el._debounceEventType = null;
  }
};

// main.js
import { createApp } from 'vue';
import App from './App.vue';
import debounceDirective from './directives/debounce';

const app = createApp(App);
// 全局注册 v-debounce 指令
app.directive('debounce', debounceDirective);
app.mount('#app');

// xxx.vue
<template>
  <button v-debounce:500="handleClick">点击防抖(500ms)</button>
  <input v-debounce.input="handleInput" placeholder="输入防抖(默认300ms)" style="margin-left: 10px;">
</template>

<script setup>
const handleClick = () => console.log('点击触发');
const handleInput = (e) => console.log('输入内容:', e.target.value);
</script>

局部注册 vue2

javascript 复制代码
<template>
  <!-- 点击防抖(500ms延迟) -->
  <button v-debounce:500="handleClick">点击触发</button>
  <!-- 输入防抖(默认300ms) -->
  <input v-debounce.input="handleInput" placeholder="输入测试">
</template>

<script>
export default {
  name: 'DebounceDemo',
  // 局部注册 v-debounce 指令
  directives: {
    debounce: {
      inserted(el, binding) {
        const fn = binding.value;
        const delay = binding.arg || 300;
        let timer = null;
        
        el._debounceHandler = () => {
          clearTimeout(timer);
          timer = setTimeout(fn, delay);
        };
        
        const eventType = binding.modifiers.input ? 'input' : 'click';
        el.addEventListener(eventType, el._debounceHandler);
        el._debounceEventType = eventType;
      },
      unbind(el) {
        clearTimeout(el.timer);
        el.removeEventListener(el._debounceEventType, el._debounceHandler);
        el._debounceHandler = el._debounceEventType = null;
      }
    }
  },
  methods: {
    handleClick() {
      console.log('点击防抖触发');
    },
    handleInput(e) {
      console.log('输入防抖触发:', e.target.value);
    }
  }
};
</script>

全局注册vue2

javascript 复制代码
// directives/debounce.js
export default {
  inserted(el, binding) {
    const fn = binding.value;
    const delay = binding.arg || 300;
    let timer = null;

    el._debounceHandler = () => {
      clearTimeout(timer);
      timer = setTimeout(fn, delay);
    };

    const eventType = binding.modifiers.input ? 'input' : 'click';
    el.addEventListener(eventType, el._debounceHandler);
    el._debounceEventType = eventType;
  },
  unbind(el) {
    clearTimeout(el.timer);
    el.removeEventListener(el._debounceEventType, el._debounceHandler);
    el._debounceHandler = el._debounceEventType = null;
  }
};

// main.js
import Vue from 'vue';
import App from './App.vue';
import debounceDirective from './directives/debounce';

// 全局注册 v-debounce 指令
Vue.directive('debounce', debounceDirective);

new Vue({ render: h => h(App) }).$mount('#app');


// vue文件中使用
<template>
  <div>
    <!-- 点击防抖(500ms延迟) -->
    <button v-debounce:500="handleClick">点击触发</button>
    
    <!-- 输入防抖(默认300ms) -->
    <input 
      v-debounce.input="handleInput" 
      placeholder="输入测试防抖"
      style="margin-left: 10px;"
    >
  </div>
</template>

<script>
export default {
  methods: {
    handleClick() {
      console.log('点击防抖触发');
    },
    handleInput(e) {
      console.log('输入内容:', e.target.value);
    }
  }
};
</script>

使用示例(Vue2/Vue3 一致):​

javascript 复制代码
<template>
  <button v-debounce:[500]="handleClick">防抖点击</button>
  <input v-debounce.input="handleSearch" placeholder="搜索防抖">
</template>

场景3:【v-img-error】图片资源报错指令兜底展示默认图标

局部注册

javascript 复制代码
// vImgError.js
// 图片加载失败指令 目前仅组件内使用,后期需要可以注册全局
// 入参即为默认图片地址,最好不要带有@符号,因为会被认为是相对路径
export const vImgError = {
    mounted(el, binging) {
        // 初次展示默认图片
        if (!el.src) el.src = binging.value
        // 监听图片加载失败事件
        el.onerror = () => {
            console.error('图片加载失败 mounted', el.src, binging.value)
            // 加载失败后展示默认图片
            el.src = binging.value
            // 移除事件监听,避免重复触发
            el.onerror = null
        }
    },
    // updated 事件可不需要,测试过
}

引入到vue3文件:
import { vImgError } from '@/directives/vImgError.js'
javascript 复制代码
// Vue 2(组件内 directives 局部注册)
directives: {
  imgError: {
    inserted(el, binding) { // 钩子:inserted
      if (!el.src) el.src = binding.value;
      el.onerror = () => {
        console.error('图片加载失败', el.src, binding.value);
        el.src = binding.value;
        el.onerror = null;
      };
    }
  }
}

全局注册

javascript 复制代码
//vue3 全局注册

import { createApp } from 'vue';
import App from './App.vue';

const app = createApp(App);

// 全局注册 v-img-error 指令
app.directive('img-error', {
  mounted(el, binding) {
    if (!binding.value) return console.warn('v-img-error 需传默认图地址');
    !el.src && (el.src = binding.value);
    el.onerror = () => {
      el.src = binding.value;
      el.onerror = null;
    };
  }
});

app.mount('#app');
javascript 复制代码
// vue2 全局注册
import Vue from 'vue';
import App from './App.vue';

// 全局注册 v-img-error 指令
Vue.directive('img-error', {
  inserted(el, binding) {
    if (!binding.value) return console.warn('v-img-error 需传默认图地址');
    !el.src && (el.src = binding.value);
    el.onerror = () => {
      el.src = binding.value;
      el.onerror = null;
    };
  }
});

new Vue({ render: h => h(App) }).$mount('#app');

局部或者全局注册指令使用都一致:

javascript 复制代码
<img class="list-logo" :src="`./assets/crypto/${item.value}_35.png`" v-img-error="'./assets/crypto/default_35.png'" alt="" />

六、注意事项(Vue2/Vue3 通用)​

  1. 避免内存泄漏:指令中绑定的事件监听(addEventListener)、定时器(setTimeout),必须在 unbind(Vue2)/unmounted(Vue3)中清理。
  2. 操作真实 DOM:指令的核心是操作 el(真实 DOM),避免通过 vnode 间接操作 DOM(vnode 是虚拟节点,仅用于描述 DOM)。
  3. 兼容性处理:iOS 等移动端场景,需注意指令触发时机(如延迟失焦、避免二次聚焦),可复用本文的 v-blur-on-outside 逻辑。
  4. 优先级:自定义指令优先级低于 Vue 内置指令(如 v-if 先执行,再执行自定义指令),多个自定义指令按注册顺序执行。

七、总结​

Vue2 和 Vue3 的自定义指令核心逻辑一致(封装 DOM 复用逻辑),主要差异集中在钩子名称、注册方式、语法糖支持上。掌握以下要点即可轻松切换:​

  1. Vue3 钩子与组件生命周期对齐(mounted/unmounted),Vue2 用 inserted/unbind。
  2. Vue3 <script setup> 中局部指令需命名为 vXXX(驼峰)。
  3. 复杂指令的事件清理逻辑,Vue2 写在 unbind,Vue3 写在 unmounted。

通过本文的双版本 Demo,你可以直接复制到对应 Vue 项目中使用,无论是 Vue2 老项目维护还是 Vue3 新项目开发,都能灵活运用自定义指令封装复用逻辑~

相关推荐
uhakadotcom2 小时前
Tomli 全面教程:常用 API 串联与实战指南
前端·面试·github
Asurplus2 小时前
【VUE】15、安装包管理工具yarn
前端·vue.js·npm·node.js·yarn
liangshanbo12152 小时前
Mac M3 安装 Antigravity Agent “已损坏“ 问题解决方案
前端·macos·antigravity
sszdlbw2 小时前
前后端在服务器的部署
运维·服务器·前端·后端
马卫斌 前端工程师2 小时前
vue 多个请求要同时拉取数据,写一个方法
前端·javascript·vue.js
苏打水com2 小时前
第十篇:Day28-30 工程化优化与部署——从“能跑”到“好用”(对标职场“项目上线”需求)
前端·css·vue·html·js
写代码的皮筏艇2 小时前
react hooks中的useState
前端·javascript
fruge2 小时前
React Fiber 架构详解:为什么它能解决页面卡顿问题?
前端·react.js·架构