Vue.js: 自定义指令

最后更新:2026-10-20

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

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

相关指令:app.directive() v-model v-for v-if

1. 你将学到


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

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

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

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)

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

(3) 收益

使用自定义指令后:


3. 自定义指令基础

(1) 3 种注册方式

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: 上一个虚拟节点
}

▶ 示例:app.directive 注册 v-highlight(用到 value 与 modifiers)

▶ 试一试

输出:

TEXT
顶部三个色块按钮「#ffe08a / #a5d8ff / #b2f2bb」,下方两行文字初始都是淡黄色底,第二行加粗。
点「#a5d8ff」→ 两行背景同时变成淡蓝色,第一行保持常规字重、第二行依然加粗——说明 .bold 修饰符只影响挂了它的那个元素。
再点「#b2f2bb」→ 两行一起变成淡绿色。

▶ 示例: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 }
})
▶ 试一试

输出:

TEXT
这段代码只定义并注册指令,没有挂载任何页面,所以页面上看不到变化。
实际使用时:v-show="false" 的元素 display 变成 none;v-text="'hi'" 写入纯文本 hi;v-html="'<b>x</b>'" 渲染出加粗的 x。

4. 5 大实战指令

(1) v-focus:自动聚焦

JS
// directives/focus.js
export const focus = {
  mounted(el, binding) {
    if (binding.value !== false) {
      el.focus()
    }
  }
}

(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)
      }
    }
  }
}

(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__)
    }
  }
}

(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__)
    }
  }
}

(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()
  }
}

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. 综合演练:三大指令联动

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

▶ 展开运行示例(80 行)
▶ 试一试

输出:

TEXT
三个区块自上而下:
v-focus — 两个输入框,打开页面时第一个(占位符「打开页面自动聚焦」)已获得焦点,500ms 后焦点自动跳到第二个;
v-copy — 按钮「复制链接」,点击后文字变「Copied!」,1.5 秒后恢复,剪贴板里是 https://web-tutorial.com;
v-debounce:800 — 按钮「保存(防抖 800ms)」和「实际触发次数: 0」,连点多次只在停止点击 800ms 后计一次,次数变 1。

(1) 📊 5 个常见错误速查

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

(2) 📊 5 大性能对比

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

相关指令:app.directive() v-model v-for v-if

❓ 常见问题

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%

🙏 帮我们做得更好

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

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