Vue.js: 自定义指令

最后更新:2026-08-26

自定义指令让你扩展 Vue 的模板语法——以 v- 开头的特殊属性,对底层 DOM 进行直接操作。Vue 内置了 v-if / v-for / v-model 等指令,你可以创建自己的 v-focus / v-permission / v-debounce 等。

自定义指令是写"底层 DOM 工具"的强大方式——它把可复用的 DOM 操作封装成声明式语法。理解 5 大生命周期钩子让你能写各种 v-directive。

1. 你将学到


2. 一个权限按钮"5 处重复"的噩梦

(1) 痛点:5 个按钮都要检查权限

Alice 的后台有 5 个按钮需要权限检查:

VUE
<!-- ❌ 翻车版:5 个按钮,5 份权限代码 -->
<template>
  <button v-if="hasPermission('user.create')" @click="createUser">Create</button>
  <button v-if="hasPermission('user.delete')" @click="deleteUser">Delete</button>
  <button v-if="hasPermission('user.edit')" @click="editUser">Edit</button>
  <button v-if="hasPermission('order.create')" @click="createOrder">Create Order</button>
  <button v-if="hasPermission('order.cancel')" @click="cancelOrder">Cancel</button>
</template>

<script setup>
function hasPermission(perm) {
  return user.value.permissions?.includes(perm)
}
</script>

5 个按钮 × 5 个权限检查 = 25 行重复代码。新增按钮还要再写 v-if。

产品经理 Charlie:

"Alice,后台有 50+ 个按钮。我们需要一个 'v-permission' 指令让代码更干净。"

(2) Vue 自定义指令解法:1 个 v-permission

JS
// directives/permission.js
export const permission = {
  mounted(el, binding) {
    const { value } = binding  // 'user.create'
    const userPermissions = getCurrentUser().permissions || []
    
    if (!userPermissions.includes(value)) {
      el.parentNode?.removeChild(el)  // 没权限就移除
    }
  }
}
JS
// main.js
import { permission } from './directives/permission'
app.directive('permission', permission)
VUE
<!-- 使用:1 个 v-permission 替代所有 v-if -->
<template>
  <button v-permission="'user.create'" @click="createUser">Create</button>
  <button v-permission="'user.delete'" @click="deleteUser">Delete</button>
  <button v-permission="'order.create'" @click="createOrder">Create Order</button>
</template>

50 个按钮 50 个 v-permission,比 50 个 v-if 简洁 50%。

(3) 收益

使用自定义指令后:


3. 自定义指令基础

(1) 3 种注册方式

JS
// 1. 全局指令(main.js)
const { createApp } = Vue
import App from './App.vue'

const app = createApp(App)

// 全局注册:所有组件都能用 v-focus
app.directive('focus', {
  mounted(el) {
    el.focus()
  }
})

app.mount('#app')
VUE
<!-- 局部指令(推荐) -->
<!-- src/components/Input.vue -->
<script setup>
// 局部注册:只有这个组件能用
const vFocus = {
  mounted(el) {
    el.focus()
  }
}
</script>

<template>
  <input v-focus>
</template>
JS
// 2. 简写(mounted + updated)
app.directive('color', (el, binding) => {
  el.style.color = binding.value
})

(2) 5 大生命周期钩子

JS
app.directive('demo', {
  // 1. created(指令创建)
  created(el, binding) {
    console.log('1. 指令创建')
  },
  
  // 2. beforeMount(元素挂载前)
  beforeMount(el) {
    console.log('2. 挂载前')
  },
  
  // 3. mounted(元素已挂载)⭐ 最常用
  mounted(el, binding) {
    console.log('3. 已挂载')
  },
  
  // 4. beforeUpdate(依赖更新前)
  beforeUpdate(el, binding) {
    console.log('4. 更新前')
  },
  
  // 5. updated(依赖更新后)
  updated(el, binding) {
    console.log('5. 已更新')
  },
  
  // 6. beforeUnmount(卸载前)
  beforeUnmount(el) {
    console.log('6. 卸载前')
  },
  
  // 7. unmounted(卸载后)⭐ 清理用
  unmounted(el) {
    console.log('7. 已卸载')
  }
})

(3) 钩子参数详解

JS
// el, binding, vnode, prevVnode 4 个参数
mounted(el, binding, vnode, prevVnode) {
  // el: 指令绑定的元素
  el.style.color = 'red'
  
  // binding: 指令信息对象
  binding.value    // 指令值,如 v-foo="bar" → bar
  binding.arg      // 参数,如 v-foo:arg → 'arg'
  binding.modifiers  // 修饰符,如 v-foo.bar → { bar: true }
  binding.instance  // 使用指令的组件实例
  binding.dir       // 指令定义对象
  
  // vnode: Vue 虚拟节点(一般不用)
  // prevVnode: 上一个虚拟节点
}

4. 5 大实战指令

(1) v-focus:自动聚焦

JS
// directives/focus.js
export const focus = {
  mounted(el, binding) {
    if (binding.value !== false) {
      el.focus()
    }
  }
}
VUE
<template>
  <!-- 1. 自动聚焦 -->
  <input v-focus>
  
  <!-- 2. 条件聚焦 -->
  <input v-focus="shouldFocus">
  
  <!-- 3. 延迟聚焦 -->
  <input v-focus:delay="500">
</template>

(2) v-permission:权限控制

JS
// directives/permission.js
import { getCurrentUser } from '@/utils/auth'

export const permission = {
  mounted(el, binding) {
    const { value, modifiers } = binding
    const user = getCurrentUser()
    
    // value: 字符串 'user.create' 或 数组 ['user.create', 'user.delete']
    // modifiers.disable: 禁用而非移除
    const required = Array.isArray(value) ? value : [value]
    const hasPermission = required.every(p => 
      user.permissions?.includes(p)
    )
    
    if (!hasPermission) {
      if (modifiers.disable) {
        el.disabled = true
        el.title = 'No permission'
      } else {
        el.parentNode?.removeChild(el)
      }
    }
  }
}
VUE
<template>
  <!-- 单权限 -->
  <button v-permission="'user.create'">Create</button>
  
  <!-- 多权限(全部满足)-->
  <button v-permission="['user.read', 'user.write']">Edit</button>
  
  <!-- 没权限时禁用(而非移除)-->
  <button v-permission.disable="'user.delete'">Delete</button>
</template>

(3) v-debounce:防抖事件

JS
// directives/debounce.js
export const debounce = {
  mounted(el, binding) {
    const { value, arg = 300 } = binding
    
    if (typeof value !== 'function') {
      console.warn('v-debounce: value must be a function')
      return
    }
    
    let timer = null
    el.__debounceTimer__ = timer
    
    el.addEventListener('click', () => {
      clearTimeout(timer)
      timer = setTimeout(() => value(), arg)
      el.__debounceTimer__ = timer
    })
  },
  
  unmounted(el) {
    if (el.__debounceTimer__) {
      clearTimeout(el.__debounceTimer__)
    }
  }
}
VUE
<template>
  <!-- v-debounce 用法:值传函数,arg 传延迟毫秒数 -->
  <button v-debounce:500="handleClick">Click me</button>
  <input v-debounce:1000="handleInput">
</template>

<script setup>
function handleClick() {
  console.log('Clicked (debounced 500ms)')
}
</script>

(4) v-copy:点击复制

JS
// directives/copy.js
export const copy = {
  mounted(el, binding) {
    const handler = async () => {
      try {
        await navigator.clipboard.writeText(binding.value)
        const original = el.textContent
        el.textContent = 'Copied!'
        setTimeout(() => { el.textContent = original }, 1500)
      } catch (err) {
        console.error('Copy failed:', err)
      }
    }
    el.addEventListener('click', handler)
    el.__copyHandler__ = handler
  },
  
  unmounted(el) {
    if (el.__copyHandler__) {
      el.removeEventListener('click', el.__copyHandler__)
    }
  }
}
VUE
<template>
  <button v-copy="shareUrl">Copy Link</button>
  <code v-copy="apiKey">Click to copy</code>
</template>

(5) v-lazy-load:图片懒加载

JS
// directives/lazyLoad.js
export const lazyLoad = {
  mounted(el, binding) {
    const observer = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) {
        el.src = binding.value
        observer.unobserve(el)
      }
    })
    observer.observe(el)
    el.__observer__ = observer
  },
  
  unmounted(el) {
    el.__observer__?.disconnect()
  }
}
VUE
<template>
  <img v-lazy-load="imageUrl" alt="...">
</template>

5. 指令参数、修饰符、值

(1) 3 种指令形态

VUE
<!-- 1. v-directive="value"(值) -->
<input v-foo="username">

<!-- 2. v-directive:arg(参数,固定字符串) -->
<input v-foo:delay="500">

<!-- 3. v-directive.modifier(修饰符,布尔对象) -->
<input v-foo.bar>

<!-- 4. 组合 -->
<input v-foo:delay.bar="500">

(2) JS 接收方式

JS
app.directive('demo', (el, binding) => {
  // v-demo="123"
  binding.value  // 123
  
  // v-demo:abc
  binding.arg  // 'abc'
  
  // v-demo.foo
  binding.modifiers  // { foo: true }
  
  // v-demo:abc.foo="123"
  binding.value    // 123
  binding.arg       // 'abc'
  binding.modifiers // { foo: true }
})

(3) 5 大组合场景

VUE
<!-- 场景 1:v-permission:disable -->
<button v-permission:disable="'user.create'">
  <!-- arg='disable', value='user.create' -->
</button>

<!-- 场景 2:v-debounce:500 -->
<button v-debounce:500="handler">
  <!-- arg='500'(500ms 防抖) -->
</button>

<!-- 场景 3:v-once.lazy -->
<img v-once.lazy="imageUrl">
<!-- modifiers.lazy=true, value=imageUrl -->
</template>

6. 完整示例:5 大指令 + 实战

▶ 示例:v-focus + v-copy + v-debounce 综合演示

HTML 📖 仅展示
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>

<style>
input, button { padding: 6px 12px; margin: 4px; border-radius: 4px; border: 1px solid #ddd; }
button { background: #42b883; color: white; border: none; cursor: pointer; }
</style>

<div id="app">
  <h4>v-focus(自动聚焦)</h4>
  <input v-focus placeholder="打开页面自动聚焦" v-model="text1">
  <input v-focus:500 placeholder="500ms 后聚焦" v-model="text2">

  <h4>v-copy(点击复制)</h4>
  <button v-copy="'https://web-tutorial.com'">复制链接</button>
  <p style="color: #999;">复制后按钮会显示"Copied!" 1.5 秒</p>

  <h4>v-debounce:800(800ms 防抖)</h4>
  <button v-debounce:800="handleSave">保存(防抖 800ms)</button>
  <p>实际触发次数: {{ saveCount }}</p>
</div>

<script>
const { createApp, ref } = Vue

// 自定义指令:v-focus(支持 arg 延迟聚焦)
const focusDirective = {
  mounted(el, binding) {
    if (binding.value === false) return
    if (binding.arg) {
      setTimeout(() => el.focus(), parseInt(binding.arg))
    } else {
      el.focus()
    }
  }
}

// 自定义指令:v-copy(点击复制文本)
const copyDirective = {
  mounted(el, binding) {
    const original = el.textContent
    const handler = async () => {
      try {
        await navigator.clipboard.writeText(binding.value)
        el.textContent = 'Copied!'
        setTimeout(() => { el.textContent = original }, 1500)
      } catch (err) {
        console.error('Copy failed:', err)
      }
    }
    el.addEventListener('click', handler)
    el._copyHandler = handler
  },
  unmounted(el) {
    if (el._copyHandler) el.removeEventListener('click', el._copyHandler)
  }
}

// 自定义指令:v-debounce(防抖事件)
const debounceDirective = {
  mounted(el, binding) {
    const fn = binding.value
    const delay = parseInt(binding.arg) || 300
    if (typeof fn !== 'function') {
      console.warn('[v-debounce] value must be a function')
      return
    }
    let timer = null
    el.addEventListener('click', () => {
      clearTimeout(timer)
      timer = setTimeout(() => fn(), delay)
    })
    el._debounceTimer = timer
  },
  unmounted(el) {
    if (el._debounceTimer) clearTimeout(el._debounceTimer)
  }
}

const app = createApp({
  setup() {
    const text1 = ref('')
    const text2 = ref('')
    const saveCount = ref(0)
    function handleSave() { saveCount.value++ }
    return { text1, text2, saveCount, handleSave }
  }
})

app.directive('focus', focusDirective)
app.directive('copy', copyDirective)
app.directive('debounce', debounceDirective)
app.mount('#app')
</script>
逻辑代码 80 行(超过 40 行限制,仅展示)

▶ 示例:5 个常见错误速查

错误 现象 解决
指令名不带 v- 不生效 v-focus(不是 focus)
value 不是函数 不执行 检查 v-debounce="handler"
忘 unmounted 清理 内存泄漏 定时器/观察者要清理
覆盖 Vue 内置指令 报错 不叫 v-if 等
指令值修改不响应 只 mounted 一次 用 updated 钩子

▶ 示例:5 大性能对比

写法 复用 性能 适用
v-if + 工具函数 ⭐⭐⭐ 1 次性
自定义指令 ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ DOM 工具
全局组件 ⭐⭐⭐ ⭐⭐⭐ 复杂 UI
Composable ⭐⭐⭐⭐ ⭐⭐⭐⭐ 业务逻辑
Pinia ⭐⭐⭐⭐ ⭐⭐⭐ 全局状态

▶ 示例:5 大内置指令借鉴(app.directive)

JS
const { createApp } = Vue
const app = createApp({})

// 1. v-show(控制 display)
app.directive('show', {
  mounted(el, binding) { el.style.display = binding.value ? '' : 'none' },
  updated(el, binding) { el.style.display = binding.value ? '' : 'none' }
})

// 2. v-text(设置 textContent)
app.directive('text', {
  mounted(el, binding) { el.textContent = binding.value },
  updated(el, binding) { el.textContent = binding.value }
})

// 3. v-html(设置 innerHTML,XSS 风险)
app.directive('html', {
  mounted(el, binding) { el.innerHTML = binding.value },
  updated(el, binding) { el.innerHTML = binding.value }
})
▶ 试一试

❓ 常见问题

Q 自定义指令和组件怎么选?
A DOM 底层操作(focus/scroll/canvas)用指令。复杂 UI(带状态/事件/数据)用组件。指令是无状态的,组件是有状态的。
Q 指令的 value 是响应式的吗?
A value 改变会触发 updated 钩子。如果要监听 value 变化,用 watch(binding.value) 或在 updated 钩子中对比新旧值。
Q v-permission:disable 怎么写?
A arg='disable',modifiers.disable=true。在指令中判断 binding.modifiers.disable 决定禁用还是移除。
Q 指令能用 TypeScript 吗?
A 能。Vue 3.3+ 支持 defineDirective + TypeScript 泛型。但通常直接用 JS 对象更简单。
Q 指令的全局 vs 局部怎么选?
A 业务专属(v-permission)用局部。通用工具(v-focus / v-copy)用全局。本教程推荐局部(更易维护)。
Q 指令能传 ref 吗?
A 能。<input v-my-directive="myRef" />,指令中 binding.value 就是 ref 对象。但不推荐(反模式)。
Q VueUse 库有指令吗?
A 有。vFocus / vClickOutside / vLazyLoad / vInfiniteScroll 等都是 VueUse 提供的指令。优先用 VueUse 而非自己写。

📖 小节


📝 作业

  1. 基础题(难度⭐) 实现 v-focus 指令:

    • 接受 arg 参数(延迟聚焦毫秒数)
    • 接受 modifiers 修饰符(prevent:阻止默认行为)
  2. 进阶题(难度⭐⭐) 实现 v-permission 完整版:

    • 支持单权限字符串:v-permission="'user.create'"
    • 支持多权限数组:v-permission="['user.read', 'user.write']"
    • 支持修饰符:.disable(禁用而非移除)
    • 配合 currentUser(provide/inject 注入)
  3. 挑战题(难度⭐⭐⭐) 实现完整的"指令库":

    1. 5 个指令:v-focus / v-permission / v-debounce / v-copy / v-lazy-load
    2. 每个指令完整实现 + TypeScript 类型
    3. 统一在 directives/index.js 导出
    4. 5 个测试用例(用 Vitest)
    5. 在实际场景中使用(电商后台 5 个不同场景)
    6. 文档化每个指令的 API 和参数
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏