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 新项目开发,都能灵活运用自定义指令封装复用逻辑~

相关推荐
EnCi Zheng12 分钟前
M5-markconv自定义CSS样式指南 [特殊字符]
前端·css·python
kyriewen16 分钟前
你的网页慢,用户不说直接走——前端性能监控教你“读心术”
前端·性能优化·监控
广州华水科技16 分钟前
北斗GNSS变形监测在大坝安全监测中的应用与优势分析
前端
前端老石人28 分钟前
前端开发中的 URL 完全指南
开发语言·前端·javascript·css·html
CAE虚拟与现实28 分钟前
五一假期闲来无事,来个前段、后端的说明吧
前端·后端·vtk·three.js·前后端
Sarvartha39 分钟前
三目运算符
linux·服务器·前端
晓晨的博客1 小时前
ROS1录制的bag包转换为ROS2格式
前端·chrome
Wect1 小时前
LeetCode 72. 编辑距离:动态规划经典题解
前端·算法·typescript
donecoding1 小时前
别再让 pnpm 跟着 nvm 跑了!独立安装终极指南
前端·node.js·前端工程化
不可能的是1 小时前
从 /simplify 指令深挖 Claude Code 多 Agent 协同机制
javascript