vue.js

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > vue.js > Vue3自定义指令

从基础到高级应用详解Vue3中自定义指令的完整指南

作者:北辰alk

自定义指令是 Vue.js 中一个强大而灵活的特性,它允许开发者直接对 DOM 元素进行底层操作,本文将深入探讨 Vue3 中自定义指令的定义方式、生命周期钩子、使用场景和最佳实践,快跟随小编一起学习一下吧

摘要

自定义指令是 Vue.js 中一个强大而灵活的特性,它允许开发者直接对 DOM 元素进行底层操作。Vue3 在保留自定义指令核心概念的同时,对其 API 进行了调整和优化,使其更符合组合式 API 的设计理念。本文将深入探讨 Vue3 中自定义指令的定义方式、生命周期钩子、使用场景和最佳实践,通过丰富的代码示例和清晰的流程图,帮助你彻底掌握这一重要特性。

一、 什么是自定义指令?为什么需要它?

1.1 自定义指令的概念

在 Vue.js 中,指令是带有 v- 前缀的特殊属性。除了 Vue 内置的指令(如 v-modelv-showv-if 等),Vue 还允许我们注册自定义指令,用于对普通 DOM 元素进行底层操作。

1.2 使用场景

自定义指令在以下场景中特别有用:

1.3 Vue2 与 Vue3 自定义指令的区别

特性Vue2Vue3
生命周期钩子bind, inserted, update, componentUpdated, unbindcreated, beforeMount, mounted, beforeUpdate, updated, beforeUnmount, unmounted
参数传递el, binding, vnode, oldVnodeel, binding, vnode, prevVnode
注册方式全局 Vue.directive(),局部 directives 选项全局 app.directive(),局部 directives 选项
与组合式API集成有限更好,可在 setup 中使用

二、 自定义指令的基本结构

2.1 指令的生命周期钩子

Vue3 中的自定义指令包含一系列生命周期钩子,这些钩子在指令的不同阶段被调用:

流程图:自定义指令生命周期

2.2 钩子函数参数

每个生命周期钩子函数都会接收以下参数:

binding 对象结构:

{
  value:        any,        // 指令的绑定值,如 v-my-directive="value"
  oldValue:     any,        // 指令绑定的前一个值
  arg:          string,     // 指令的参数,如 v-my-directive:arg
  modifiers:    object,     // 指令的修饰符对象,如 v-my-directive.modifier
  instance:     Component,  // 使用指令的组件实例
  dir:          object      // 指令的定义对象
}

三、 定义自定义指令的多种方式

3.1 全局自定义指令

全局指令在整个 Vue 应用中都可用。

方式一:使用app.directive()

// main.js
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

// 定义全局焦点指令
app.directive('focus', {
  mounted(el) {
    el.focus()
    console.log('元素获得焦点')
  }
})

// 定义全局颜色指令(带参数和值)
app.directive('color', {
  beforeMount(el, binding) {
    el.style.color = binding.value
  },
  updated(el, binding) {
    el.style.color = binding.value
  }
})

app.mount('#app')

方式二:使用插件形式

// directives/index.js
export const focusDirective = {
  mounted(el) {
    el.focus()
  }
}

export const colorDirective = {
  beforeMount(el, binding) {
    el.style.color = binding.value
  },
  updated(el, binding) {
    el.style.color = binding.value
  }
}

// 注册所有指令
export function registerDirectives(app) {
  app.directive('focus', focusDirective)
  app.directive('color', colorDirective)
}

// main.js
import { createApp } from 'vue'
import App from './App.vue'
import { registerDirectives } from './directives'

const app = createApp(App)
registerDirectives(app)
app.mount('#app')

3.2 局部自定义指令

局部指令只在特定组件中可用。

选项式 API

<template>
  <div>
    <input v-focus-local placeholder="局部焦点指令" />
    <p v-color-local="textColor">这个文本颜色会变化</p>
    <button @click="changeColor">改变颜色</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      textColor: 'red'
    }
  },
  methods: {
    changeColor() {
      this.textColor = this.textColor === 'red' ? 'blue' : 'red'
    }
  },
  directives: {
    // 局部焦点指令
    'focus-local': {
      mounted(el) {
        el.focus()
      }
    },
    // 局部颜色指令
    'color-local': {
      beforeMount(el, binding) {
        el.style.color = binding.value
      },
      updated(el, binding) {
        el.style.color = binding.value
      }
    }
  }
}
</script>

组合式 API

<template>
  <div>
    <input v-focus-local placeholder="局部焦点指令" />
    <p v-color-local="textColor">这个文本颜色会变化</p>
    <button @click="changeColor">改变颜色</button>
  </div>
</template>

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

const textColor = ref('red')

const changeColor = () => {
  textColor.value = textColor.value === 'red' ? 'blue' : 'red'
}

// 局部自定义指令
const vFocusLocal = {
  mounted(el) {
    el.focus()
  }
}

const vColorLocal = {
  beforeMount(el, binding) {
    el.style.color = binding.value
  },
  updated(el, binding) {
    el.style.color = binding.value
  }
}
</script>

四、 完整生命周期示例

让我们通过一个完整的示例来演示所有生命周期钩子的使用:

<template>
  <div class="demo-container">
    <h2>自定义指令完整生命周期演示</h2>
    
    <div>
      <button @click="toggleDisplay">{{ isVisible ? '隐藏' : '显示' }}元素</button>
      <button @click="changeMessage">改变消息</button>
      <button @click="changeColor">改变颜色</button>
    </div>

    <div v-if="isVisible" v-lifecycle-demo:arg.modifier="directiveValue" 
         class="demo-element" :style="{ color: elementColor }">
      {{ message }}
    </div>

    <div class="log-container">
      <h3>生命周期日志:</h3>
      <div v-for="(log, index) in logs" :key="index" class="log-item">
        {{ log }}
      </div>
    </div>
  </div>
</template>

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

const isVisible = ref(false)
const message = ref('Hello, Custom Directive!')
const elementColor = ref('#333')
const logs = ref([])

const directiveValue = reactive({
  text: '指令值对象',
  count: 0
})

// 添加日志函数
const addLog = (hookName, el, binding) => {
  const log = `[${new Date().toLocaleTimeString()}] ${hookName}: value=${JSON.stringify(binding.value)}, arg=${binding.arg}`
  logs.value.push(log)
  // 保持日志数量不超过20条
  if (logs.value.length > 20) {
    logs.value.shift()
  }
}

// 完整的生命周期指令
const vLifecycleDemo = {
  created(el, binding) {
    addLog('created', el, binding)
    console.log('created - 指令创建,元素还未挂载')
  },
  
  beforeMount(el, binding) {
    addLog('beforeMount', el, binding)
    console.log('beforeMount - 元素挂载前')
    el.style.transition = 'all 0.3s ease'
  },
  
  mounted(el, binding) {
    addLog('mounted', el, binding)
    console.log('mounted - 元素挂载完成')
    console.log('修饰符:', binding.modifiers)
    console.log('参数:', binding.arg)
    
    // 添加动画效果
    el.style.opacity = '0'
    el.style.transform = 'translateY(-20px)'
    
    setTimeout(() => {
      el.style.opacity = '1'
      el.style.transform = 'translateY(0)'
    }, 100)
  },
  
  beforeUpdate(el, binding) {
    addLog('beforeUpdate', el, binding)
    console.log('beforeUpdate - 元素更新前')
  },
  
  updated(el, binding) {
    addLog('updated', el, binding)
    console.log('updated - 元素更新完成')
    
    // 更新时的动画
    el.style.backgroundColor = '#e3f2fd'
    setTimeout(() => {
      el.style.backgroundColor = ''
    }, 500)
  },
  
  beforeUnmount(el, binding) {
    addLog('beforeUnmount', el, binding)
    console.log('beforeUnmount - 元素卸载前')
    
    // 卸载动画
    el.style.opacity = '1'
    el.style.transform = 'translateY(0)'
    el.style.opacity = '0'
    el.style.transform = 'translateY(-20px)'
  },
  
  unmounted(el, binding) {
    addLog('unmounted', el, binding)
    console.log('unmounted - 元素卸载完成')
  }
}

const toggleDisplay = () => {
  isVisible.value = !isVisible.value
}

const changeMessage = () => {
  message.value = `消息已更新 ${Date.now()}`
  directiveValue.count++
}

const changeColor = () => {
  const colors = ['#ff4444', '#44ff44', '#4444ff', '#ff44ff', '#ffff44']
  elementColor.value = colors[Math.floor(Math.random() * colors.length)]
}
</script>

<style scoped>
.demo-container {
  padding: 20px;
  max-width: 800px;
  margin: 0 auto;
}

.demo-element {
  padding: 20px;
  margin: 20px 0;
  border: 2px solid #42b983;
  border-radius: 8px;
  background: #f9f9f9;
}

.log-container {
  margin-top: 20px;
  padding: 15px;
  background: #f5f5f5;
  border-radius: 8px;
  max-height: 400px;
  overflow-y: auto;
}

.log-item {
  padding: 5px 10px;
  margin: 2px 0;
  background: white;
  border-radius: 4px;
  font-family: 'Courier New', monospace;
  font-size: 12px;
}

button {
  margin: 5px;
  padding: 8px 16px;
  background: #42b983;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

button:hover {
  background: #369870;
}
</style>

五、 实用自定义指令示例

5.1 点击外部关闭指令

<template>
  <div class="click-outside-demo">
    <h2>点击外部关闭演示</h2>
    
    <button @click="showDropdown = !showDropdown">
      切换下拉菜单 {{ showDropdown ? '▲' : '▼' }}
    </button>

    <div v-if="showDropdown" v-click-outside="closeDropdown" class="dropdown">
      <div class="dropdown-item">菜单项 1</div>
      <div class="dropdown-item">菜单项 2</div>
      <div class="dropdown-item">菜单项 3</div>
    </div>

    <div v-if="showModal" v-click-outside="closeModal" class="modal">
      <div class="modal-content">
        <h3>模态框</h3>
        <p>点击模态框外部可以关闭</p>
        <button @click="showModal = false">关闭</button>
      </div>
    </div>

    <button @click="showModal = true" style="margin-left: 10px;">
      打开模态框
    </button>
  </div>
</template>

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

const showDropdown = ref(false)
const showModal = ref(false)

// 点击外部关闭指令
const vClickOutside = {
  mounted(el, binding) {
    el._clickOutsideHandler = (event) => {
      // 检查点击是否在元素外部
      if (!(el === event.target || el.contains(event.target))) {
        binding.value(event)
      }
    }
    document.addEventListener('click', el._clickOutsideHandler)
  },
  unmounted(el) {
    document.removeEventListener('click', el._clickOutsideHandler)
  }
}

const closeDropdown = () => {
  showDropdown.value = false
}

const closeModal = () => {
  showModal.value = false
}
</script>

<style scoped>
.dropdown {
  position: absolute;
  top: 100%;
  left: 0;
  background: white;
  border: 1px solid #ccc;
  border-radius: 4px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  z-index: 1000;
  margin-top: 5px;
}

.dropdown-item {
  padding: 10px 20px;
  cursor: pointer;
  border-bottom: 1px solid #eee;
}

.dropdown-item:hover {
  background: #f5f5f5;
}

.dropdown-item:last-child {
  border-bottom: none;
}

.modal {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0,0,0,0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 2000;
}

.modal-content {
  background: white;
  padding: 30px;
  border-radius: 8px;
  max-width: 400px;
  width: 90%;
}
</style>

5.2 输入限制指令

<template>
  <div class="input-restriction-demo">
    <h2>输入限制指令演示</h2>
    
    <div class="input-group">
      <label>仅数字输入:</label>
      <input v-number-only v-model="numberInput" placeholder="只能输入数字" />
      <span>值: {{ numberInput }}</span>
    </div>

    <div class="input-group">
      <label>最大长度限制:</label>
      <input v-limit-length="10" v-model="limitedInput" placeholder="最多10个字符" />
      <span>值: {{ limitedInput }}</span>
    </div>

    <div class="input-group">
      <label>禁止特殊字符:</label>
      <input v-no-special-chars v-model="noSpecialInput" placeholder="不能输入特殊字符" />
      <span>值: {{ noSpecialInput }}</span>
    </div>

    <div class="input-group">
      <label>自动格式化手机号:</label>
      <input v-phone-format v-model="phoneInput" placeholder="输入手机号" />
      <span>值: {{ phoneInput }}</span>
    </div>
  </div>
</template>

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

const numberInput = ref('')
const limitedInput = ref('')
const noSpecialInput = ref('')
const phoneInput = ref('')

// 仅数字输入指令
const vNumberOnly = {
  mounted(el) {
    el.addEventListener('input', (e) => {
      e.target.value = e.target.value.replace(/[^\d]/g, '')
      // 触发 v-model 更新
      e.dispatchEvent(new Event('input'))
    })
  }
}

// 长度限制指令
const vLimitLength = {
  mounted(el, binding) {
    const maxLength = binding.value
    el.setAttribute('maxlength', maxLength)
    
    el.addEventListener('input', (e) => {
      if (e.target.value.length > maxLength) {
        e.target.value = e.target.value.slice(0, maxLength)
        e.dispatchEvent(new Event('input'))
      }
    })
  }
}

// 禁止特殊字符指令
const vNoSpecialChars = {
  mounted(el) {
    el.addEventListener('input', (e) => {
      e.target.value = e.target.value.replace(/[^a-zA-Z0-9\u4e00-\u9fa5]/g, '')
      e.dispatchEvent(new Event('input'))
    })
  }
}

// 手机号格式化指令
const vPhoneFormat = {
  mounted(el) {
    el.addEventListener('input', (e) => {
      let value = e.target.value.replace(/\D/g, '')
      
      if (value.length > 3 && value.length <= 7) {
        value = value.replace(/(\d{3})(\d+)/, '$1-$2')
      } else if (value.length > 7) {
        value = value.replace(/(\d{3})(\d{4})(\d+)/, '$1-$2-$3')
      }
      
      e.target.value = value
      e.dispatchEvent(new Event('input'))
    })
  }
}
</script>

<style scoped>
.input-restriction-demo {
  padding: 20px;
}

.input-group {
  margin: 15px 0;
}

label {
  display: inline-block;
  width: 150px;
  font-weight: bold;
}

input {
  padding: 8px;
  border: 1px solid #ddd;
  border-radius: 4px;
  margin: 0 10px;
  width: 200px;
}

span {
  color: #666;
  font-size: 14px;
}
</style>

5.3 权限控制指令

<template>
  <div class="permission-demo">
    <h2>权限控制指令演示</h2>
    
    <div class="user-info">
      <label>当前用户角色:</label>
      <select v-model="currentRole" @change="updatePermissions">
        <option value="guest">游客</option>
        <option value="user">普通用户</option>
        <option value="editor">编辑者</option>
        <option value="admin">管理员</option>
      </select>
    </div>

    <div class="permission-list">
      <h3>可用功能:</h3>
      
      <button v-permission="'view'" class="feature-btn">
        🔍 查看内容
      </button>
      
      <button v-permission="'edit'" class="feature-btn">
        ✏️ 编辑内容
      </button>
      
      <button v-permission="'delete'" class="feature-btn">
        🗑️ 删除内容
      </button>
      
      <button v-permission="'admin'" class="feature-btn">
        ⚙️ 系统管理
      </button>
      
      <button v-permission="['edit', 'delete']" class="feature-btn">
        🔄 批量操作
      </button>
    </div>

    <div class="current-permissions">
      <h3>当前权限:</h3>
      <ul>
        <li v-for="permission in currentPermissions" :key="permission">
          {{ permission }}
        </li>
      </ul>
    </div>
  </div>
</template>

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

// 角色权限映射
const rolePermissions = {
  guest: ['view'],
  user: ['view', 'edit'],
  editor: ['view', 'edit', 'delete'],
  admin: ['view', 'edit', 'delete', 'admin']
}

const currentRole = ref('user')
const currentPermissions = ref(['view', 'edit'])

// 权限控制指令
const vPermission = {
  mounted(el, binding) {
    checkPermission(el, binding)
  },
  updated(el, binding) {
    checkPermission(el, binding)
  }
}

// 检查权限函数
const checkPermission = (el, binding) => {
  const requiredPermissions = Array.isArray(binding.value) 
    ? binding.value 
    : [binding.value]
  
  const hasPermission = requiredPermissions.some(permission => 
    currentPermissions.value.includes(permission)
  )
  
  if (!hasPermission) {
    el.style.display = 'none'
  } else {
    el.style.display = 'inline-block'
  }
}

// 更新权限
const updatePermissions = () => {
  currentPermissions.value = rolePermissions[currentRole.value] || []
}
</script>

<style scoped>
.permission-demo {
  padding: 20px;
  max-width: 600px;
  margin: 0 auto;
}

.user-info {
  margin: 20px 0;
}

select {
  padding: 8px;
  border: 1px solid #ddd;
  border-radius: 4px;
  margin-left: 10px;
}

.permission-list {
  margin: 30px 0;
}

.feature-btn {
  display: inline-block;
  padding: 12px 20px;
  margin: 5px;
  background: #42b983;
  color: white;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-size: 14px;
  transition: background 0.3s;
}

.feature-btn:hover {
  background: #369870;
}

.current-permissions {
  margin-top: 30px;
  padding: 15px;
  background: #f5f5f5;
  border-radius: 8px;
}

.current-permissions ul {
  list-style: none;
  padding: 0;
}

.current-permissions li {
  padding: 5px 10px;
  background: white;
  margin: 5px 0;
  border-radius: 4px;
  border-left: 4px solid #42b983;
}
</style>

六、 高级技巧与最佳实践

6.1 指令参数动态化

<template>
  <div>
    <input v-tooltip="tooltipConfig" placeholder="悬浮显示提示" />
    
    <div v-pin="pinConfig" class="pinned-element">
      可动态配置的固定元素
    </div>
    
    <button @click="updateConfig">更新配置</button>
  </div>
</template>

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

// 动态提示指令
const vTooltip = {
  mounted(el, binding) {
    const config = binding.value
    el.title = config.text
    el.style.cursor = config.cursor || 'help'
    
    if (config.position) {
      el.dataset.position = config.position
    }
  },
  updated(el, binding) {
    const config = binding.value
    el.title = config.text
  }
}

// 动态固定指令
const vPin = {
  mounted(el, binding) {
    updatePinPosition(el, binding)
  },
  updated(el, binding) {
    updatePinPosition(el, binding)
  }
}

const updatePinPosition = (el, binding) => {
  const config = binding.value
  el.style.position = 'fixed'
  el.style[config.side] = config.distance + 'px'
  el.style.zIndex = config.zIndex || 1000
}

const tooltipConfig = reactive({
  text: '这是一个动态提示',
  cursor: 'help',
  position: 'top'
})

const pinConfig = reactive({
  side: 'top',
  distance: 20,
  zIndex: 1000
})

const updateConfig = () => {
  tooltipConfig.text = `更新后的提示 ${Date.now()}`
  pinConfig.side = pinConfig.side === 'top' ? 'bottom' : 'top'
  pinConfig.distance = Math.random() * 100 + 20
}
</script>

6.2 指令组合与复用

// directives/composable.js
export function useClickHandlers() {
  return {
    mounted(el, binding) {
      el._clickHandler = binding.value
      el.addEventListener('click', el._clickHandler)
    },
    unmounted(el) {
      el.removeEventListener('click', el._clickHandler)
    }
  }
}

export function useHoverHandlers() {
  return {
    mounted(el, binding) {
      el._mouseenterHandler = binding.value.enter
      el._mouseleaveHandler = binding.value.leave
      
      if (el._mouseenterHandler) {
        el.addEventListener('mouseenter', el._mouseenterHandler)
      }
      if (el._mouseleaveHandler) {
        el.addEventListener('mouseleave', el._mouseleaveHandler)
      }
    },
    unmounted(el) {
      if (el._mouseenterHandler) {
        el.removeEventListener('mouseenter', el._mouseenterHandler)
      }
      if (el._mouseleaveHandler) {
        el.removeEventListener('mouseleave', el._mouseleaveHandler)
      }
    }
  }
}

// 组合指令
export const vInteractive = {
  mounted(el, binding) {
    const { click, hover } = binding.value
    
    if (click) {
      el.addEventListener('click', click)
      el._clickHandler = click
    }
    
    if (hover) {
      el.addEventListener('mouseenter', hover.enter)
      el.addEventListener('mouseleave', hover.leave)
      el._hoverHandlers = hover
    }
  },
  unmounted(el) {
    if (el._clickHandler) {
      el.removeEventListener('click', el._clickHandler)
    }
    if (el._hoverHandlers) {
      el.removeEventListener('mouseenter', el._hoverHandlers.enter)
      el.removeEventListener('mouseleave', el._hoverHandlers.leave)
    }
  }
}

七、 总结

7.1 核心要点回顾

7.2 最佳实践

7.3 适用场景

自定义指令是 Vue.js 生态中一个非常强大的特性,合理使用可以极大地提高代码的复用性和可维护性。

以上就是从基础到高级应用详解Vue3中自定义指令的完整指南的详细内容,更多关于Vue3自定义指令的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文