自定义指令 #

一、指令简介 #

自定义指令用于直接操作DOM,当内置指令无法满足需求时可以使用自定义指令。

1.1 注册方式 #

javascript
// 全局注册
app.directive('focus', {
  mounted(el) {
    el.focus()
  }
})

// 局部注册
export default {
  directives: {
    focus: {
      mounted(el) {
        el.focus()
      }
    }
  }
}

1.2 使用指令 #

vue
<template>
  <input v-focus>
</template>

二、指令钩子函数 #

2.1 钩子列表 #

javascript
const myDirective = {
  // 绑定元素的父组件挂载前
  created(el, binding, vnode, prevVnode) {},
  
  // 绑定元素的父组件挂载前
  beforeMount(el, binding, vnode, prevVnode) {},
  
  // 绑定元素的父组件挂载时
  mounted(el, binding, vnode, prevVnode) {},
  
  // 父组件更新前
  beforeUpdate(el, binding, vnode, prevVnode) {},
  
  // 父组件更新后
  updated(el, binding, vnode, prevVnode) {},
  
  // 父组件卸载前
  beforeUnmount(el, binding, vnode, prevVnode) {},
  
  // 父组件卸载后
  unmounted(el, binding, vnode, prevVnode) {}
}

2.2 钩子参数 #

javascript
app.directive('demo', {
  mounted(el, binding, vnode) {
    // el: 绑定的元素
    console.log(el)
    
    // binding: 绑定对象
    console.log(binding.value)      // 指令值
    console.log(binding.oldValue)   // 之前的值
    console.log(binding.arg)        // 指令参数 v-demo:foo
    console.log(binding.modifiers)  // 修饰符对象 { foo: true }
    console.log(binding.instance)   // 组件实例
    console.log(binding.dir)        // 指令定义对象
    
    // vnode: 虚拟节点
    console.log(vnode)
  }
})

三、常用自定义指令 #

3.1 自动聚焦 #

javascript
// directives/focus.js
export const vFocus = {
  mounted(el) {
    el.focus()
  }
}
vue
<template>
  <input v-focus>
</template>

<script setup>
import { vFocus } from './directives/focus'
</script>

3.2 防抖指令 #

javascript
// directives/debounce.js
export const vDebounce = {
  mounted(el, binding) {
    const { value, arg = 'click' } = binding
    const delay = arg ? parseInt(arg) : 300
    
    let timer = null
    
    el.addEventListener('click', () => {
      if (timer) clearTimeout(timer)
      timer = setTimeout(() => {
        value()
      }, delay)
    })
  },
  
  unmounted(el) {
    el.removeEventListener('click')
  }
}
vue
<template>
  <button v-debounce="handleClick">防抖按钮</button>
  <button v-debounce:500="handleClick">500ms防抖</button>
</template>

3.3 权限指令 #

javascript
// directives/permission.js
export const vPermission = {
  mounted(el, binding) {
    const { value } = binding
    const permissions = JSON.parse(localStorage.getItem('permissions') || '[]')
    
    if (!permissions.includes(value)) {
      el.parentNode?.removeChild(el)
    }
  }
}
vue
<template>
  <button v-permission="'admin'">管理员可见</button>
  <button v-permission="'editor'">编辑可见</button>
</template>

3.4 点击外部指令 #

javascript
// directives/clickOutside.js
export const vClickOutside = {
  mounted(el, binding) {
    el._clickOutside = (event) => {
      if (!el.contains(event.target)) {
        binding.value(event)
      }
    }
    document.addEventListener('click', el._clickOutside)
  },
  
  unmounted(el) {
    document.removeEventListener('click', el._clickOutside)
  }
}
vue
<template>
  <div v-click-outside="handleClickOutside">
    点击外部会触发
  </div>
</template>

3.5 复制指令 #

javascript
// directives/copy.js
export const vCopy = {
  mounted(el, binding) {
    el._copyValue = binding.value
    
    el.addEventListener('click', async () => {
      try {
        await navigator.clipboard.writeText(el._copyValue)
        console.log('复制成功')
      } catch (err) {
        console.error('复制失败:', err)
      }
    })
  },
  
  updated(el, binding) {
    el._copyValue = binding.value
  }
}
vue
<template>
  <button v-copy="textToCopy">复制文本</button>
</template>

3.6 懒加载指令 #

javascript
// directives/lazy.js
export const vLazy = {
  mounted(el, binding) {
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          el.src = binding.value
          observer.unobserve(el)
        }
      })
    })
    
    observer.observe(el)
    el._observer = observer
  },
  
  unmounted(el) {
    if (el._observer) {
      el._observer.unobserve(el)
    }
  }
}
vue
<template>
  <img v-lazy="imageUrl" alt="懒加载图片">
</template>

3.7 长按指令 #

javascript
// directives/longpress.js
export const vLongpress = {
  mounted(el, binding) {
    const { value, arg } = binding
    const duration = arg ? parseInt(arg) : 500
    
    let timer = null
    
    const start = (e) => {
      if (e.type === 'click' && e.button !== 0) return
      
      if (timer === null) {
        timer = setTimeout(() => {
          value()
        }, duration)
      }
    }
    
    const cancel = () => {
      if (timer !== null) {
        clearTimeout(timer)
        timer = null
      }
    }
    
    el.addEventListener('mousedown', start)
    el.addEventListener('touchstart', start)
    el.addEventListener('click', cancel)
    el.addEventListener('mouseout', cancel)
    el.addEventListener('touchend', cancel)
    el.addEventListener('touchcancel', cancel)
    
    el._cleanup = () => {
      el.removeEventListener('mousedown', start)
      el.removeEventListener('touchstart', start)
      el.removeEventListener('click', cancel)
      el.removeEventListener('mouseout', cancel)
      el.removeEventListener('touchend', cancel)
      el.removeEventListener('touchcancel', cancel)
    }
  },
  
  unmounted(el) {
    el._cleanup?.()
  }
}
vue
<template>
  <button v-longpress="handleLongPress">长按触发</button>
  <button v-longpress:1000="handleLongPress">长按1秒触发</button>
</template>

3.8 工具提示指令 #

javascript
// directives/tooltip.js
export const vTooltip = {
  mounted(el, binding) {
    const tooltip = document.createElement('div')
    tooltip.className = 'tooltip'
    tooltip.textContent = binding.value
    tooltip.style.cssText = `
      position: absolute;
      background: #333;
      color: #fff;
      padding: 5px 10px;
      border-radius: 4px;
      font-size: 12px;
      z-index: 1000;
      display: none;
    `
    
    document.body.appendChild(tooltip)
    
    const show = () => {
      const rect = el.getBoundingClientRect()
      tooltip.style.display = 'block'
      tooltip.style.left = rect.left + 'px'
      tooltip.style.top = rect.bottom + 5 + 'px'
    }
    
    const hide = () => {
      tooltip.style.display = 'none'
    }
    
    el.addEventListener('mouseenter', show)
    el.addEventListener('mouseleave', hide)
    
    el._tooltip = tooltip
    el._showTooltip = show
    el._hideTooltip = hide
  },
  
  updated(el, binding) {
    if (el._tooltip) {
      el._tooltip.textContent = binding.value
    }
  },
  
  unmounted(el) {
    if (el._tooltip) {
      el._tooltip.remove()
    }
    el.removeEventListener('mouseenter', el._showTooltip)
    el.removeEventListener('mouseleave', el._hideTooltip)
  }
}
vue
<template>
  <button v-tooltip="提示文本">悬停显示提示</button>
</template>

四、函数简写 #

4.1 简写形式 #

javascript
// 只在mounted和updated时触发
app.directive('color', (el, binding) => {
  el.style.color = binding.value
})
vue
<template>
  <p v-color="textColor">彩色文字</p>
</template>

4.2 对象字面量 #

javascript
app.directive('style', (el, binding) => {
  Object.assign(el.style, binding.value)
})
vue
<template>
  <div v-style="{ color: 'red', fontSize: '20px' }">
    样式文字
  </div>
</template>

五、指令组合式函数 #

javascript
// composables/useDirective.js
import { onMounted, onUnmounted } from 'vue'

export function useClickOutside(elementRef, callback) {
  const handleClick = (event) => {
    if (elementRef.value && !elementRef.value.contains(event.target)) {
      callback(event)
    }
  }
  
  onMounted(() => {
    document.addEventListener('click', handleClick)
  })
  
  onUnmounted(() => {
    document.removeEventListener('click', handleClick)
  })
}
vue
<script setup>
import { ref } from 'vue'
import { useClickOutside } from './composables/useDirective'

const dropdownRef = ref(null)
const isOpen = ref(false)

useClickOutside(dropdownRef, () => {
  isOpen.value = false
})
</script>

六、总结 #

指令钩子 #

钩子 触发时机
created 绑定元素的父组件挂载前
beforeMount 绑定元素的父组件挂载前
mounted 绑定元素的父组件挂载时
beforeUpdate 父组件更新前
updated 父组件更新后
beforeUnmount 父组件卸载前
unmounted 父组件卸载后

自定义指令要点:

  • 用于直接操作DOM
  • 注意在unmounted中清理事件监听
  • 使用binding获取指令参数和值
  • 复杂逻辑考虑使用组合式函数
最后更新:2026-03-26