前言

在 Vue 开发中,我们通常遵循 数据驱动视图 的理念,很少直接操作 DOM。但有些场景却绕不开 DOM 操作,比如:输入框自动获取焦点、点击外部区域关闭下拉框、图片懒加载、权限控制等。这些场景如果写在组件内部,会导致代码冗余、逻辑分散。

自定义指令正是为解决这类问题而生,它提供了一种优雅的方式来封装 DOM 操作逻辑,让代码更加简洁、可复用。本文将深入探讨自定义指令的生命周期、钩子参数,并通过大量实战案例,帮我们掌握这一强大的抽象工具。

什么时候需要自定义指令?

直接操作 DOM 的场景

虽然 Vue 团队官方推崇数据驱动,而且建议不要在 Vue 项目中直接操作 DOM ,但有些操作又必须直接操作 DOM:

<template>
  <input ref="inputRef" />
</template>

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

const inputRef = ref(null)

onMounted(() => {
  // 每次都要写这个逻辑
  inputRef.value?.focus()
})
</script>

此时,如果用自定义指令,就可以优雅地解决这个问题:

<template>
  <!--  声明式的指令 -->
  <input v-focus />
</template>

<script setup>
// 指令只定义一次,到处使用
</script>

与第三方库集成

当我们需要集成非 Vue 的第三方库时,自定义指令是理想的桥梁:

// 集成 Clipboard.js 复制功能
app.directive('clipboard', {
  mounted(el, binding) {
    const clipboard = new ClipboardJS(el, {
      text: () => binding.value
    })
    
    clipboard.on('success', () => {
      alert('复制成功')
    })
  }
})

复用 DOM 相关的逻辑

有些 DOM 操作逻辑需要在多个组件中重复使用,比如:

  • 点击外部关闭:下拉菜单、模态框
  • 滚动加载更多:无限滚动列表
  • 拖拽调整大小:可调整尺寸的面板
  • 权限控制:根据权限隐藏元素

将这些逻辑封装成指令,就可以实现 一次定义,到处使用

指令的生命周期钩子

七个钩子函数详解

自定义指令提供了七个钩子函数,覆盖了指令从创建到销毁的完整过程:

const myDirective = {
  // 在绑定元素的 attribute 或事件器被应用之前调用
  created(el, binding, vnode) {
    console.log('created')
  },
  
  // 在元素被插入到 DOM 前调用
  beforeMount(el, binding, vnode) {
    console.log('beforeMount')
  },
  
  // 在绑定元素的父组件及他自己的所有子节点都挂载完成后调用
  mounted(el, binding, vnode) {
    console.log('mounted')
  },
  
  // 在包含组件的 VNode 更新之前调用
  beforeUpdate(el, binding, vnode, prevVnode) {
    console.log('beforeUpdate')
  },
  
  // 在包含组件的 VNode 及其子组件的 VNode 更新之后调用
  updated(el, binding, vnode, prevVnode) {
    console.log('updated')
  },
  
  // 在绑定元素的父组件卸载前调用
  beforeUnmount(el, binding, vnode) {
    console.log('beforeUnmount')
  },
  
  // 在绑定元素的父组件卸载后调用
  unmounted(el, binding, vnode) {
    console.log('unmounted')
  }
}

每个钩子的适用场景

钩子函数适用场景示例
created初始化只在 JS 层面工作的内容添加事件前的准备
beforeMount第一次 DOM 渲染前的修改设置初始样式
mounted最常用,操作真实 DOM获取焦点、事件、初始化第三方库
beforeUpdate基于更新的响应式数据修改 DOM更新前的数据验证
updated响应式数据更新后的操作更新第三方库的配置
beforeUnmount清理前的最后操作保存状态、执行动画
unmounted清理工作移除事件、销毁实例

钩子函数的参数详解

每个钩子函数都接收相同的参数:

interface Binding {
  value: any      // 传递给指令的值
  oldValue: any   // 之前的值,仅在 beforeUpdate 和 updated 中可用
  arg: string     // 传递给指令的参数
  modifiers: { [key: string]: boolean } // 修饰符对象
  instance: ComponentPublicInstance // 使用该指令的组件实例
  dir: Object     // 指令的定义对象
}

function hook(
  el: HTMLElement,           // 指令绑定的元素
  binding: Binding,          // 包含指令所有信息的对象
  vnode: VNode,              // 元素的虚拟节点
  prevVnode: VNode | null    // 上一个虚拟节点,仅在 update 钩子中可用
) {}

钩子函数的参数使用示例

<template>
  <div 
    v-demo:foo.bar.baz="value"
    @click="value++"
  >
    点击增加
  </div>
</template>

<script setup>
const value = ref(1)
</script>

<!-- 指令实现 -->
<script>
app.directive('demo', {
  mounted(el, binding) {
    console.log(binding.value)     // 1
    console.log(binding.arg)       // 'foo'
    console.log(binding.modifiers) // { bar: true, baz: true }
    console.log(binding.instance)  // 当前组件实例
  },
  
  updated(el, binding) {
    console.log(binding.value)     // 更新后的值
    console.log(binding.oldValue)  // 更新前的值
  }
})
</script>

实用自定义指令实战

v-focus:自动获取焦点

最简单的实用指令:

// focus.ts
export const vFocus = {
  mounted: (el: HTMLElement) => {
    el.focus()
  }
}

// 或者支持延迟获取焦点
export const vFocus = {
  mounted(el: HTMLElement, binding: { value?: number }) {
    if (binding.value) {
      setTimeout(() => el.focus(), binding.value)
    } else {
      el.focus()
    }
  }
}

使用

<template>
  <!-- 立即获取焦点 -->
  <input v-focus />
  
  <!-- 延迟500ms获取焦点 -->
  <input v-focus="500" />
</template>

v-click-outside:点击外部关闭

这是最常用的指令之一,用于下拉菜单、模态框等:

// click-outside.ts
export const vClickOutside = {
  mounted(el: HTMLElement, binding: { value: () => void }) {
    // 使用自定义属性保存处理函数,方便移除
    el._clickOutsideHandler = (event: Event) => {
      // 点击的元素不是目标元素本身也不是它的子元素
      if (!el.contains(event.target as Node)) {
        binding.value()
      }
    }
    
    // 使用 setTimeout 确保不捕获触发绑定的事件
    setTimeout(() => {
      document.addEventListener('click', el._clickOutsideHandler)
    }, 0)
  },
  
  unmounted(el: HTMLElement) {
    document.removeEventListener('click', el._clickOutsideHandler)
    delete el._clickOutsideHandler
  }
}

增强版:支持排除特定元素

export const vClickOutside = {
  mounted(el: HTMLElement, binding: { 
    value: () => void,
    arg?: string  // 排除的选择器
  }) {
    el._clickOutsideHandler = (event: Event) => {
      const target = event.target as HTMLElement
      
      // 检查是否点击了排除元素
      if (binding.arg) {
        const excludeEl = document.querySelector(binding.arg)
        if (excludeEl?.contains(target)) {
          return
        }
      }
      
      if (!el.contains(target)) {
        binding.value()
      }
    }
    
    setTimeout(() => {
      document.addEventListener('click', el._clickOutsideHandler)
    }, 0)
  },
  
  unmounted(el: HTMLElement) {
    document.removeEventListener('click', el._clickOutsideHandler)
  }
}

使用

<template>
  <div class="dropdown">
    <button ref="toggleBtn">菜单</button>
    
    <div 
      v-click-outside="closeDropdown"
      v-click-outside:".toggle-btn"="closeDropdown"
      class="dropdown-menu"
    >
      <ul>
        <li>选项1</li>
        <li>选项2</li>
      </ul>
    </div>
  </div>
</template>

v-debounce:输入防抖

处理输入框的高频事件:

// debounce.ts
export const vDebounce = {
  mounted(el: HTMLInputElement, binding: { 
    value: (...args: any[]) => void,
    arg?: string  // 事件类型,默认 'input'
  }) {
    const eventType = binding.arg || 'input'
    let timer: ReturnType<typeof setTimeout> | null = null
    
    el._debounceHandler = (event: Event) => {
      if (timer) clearTimeout(timer)
      
      timer = setTimeout(() => {
        binding.value(event)
        timer = null
      }, 300) // 固定300ms,也可以从 binding.value 获取
    }
    
    el.addEventListener(eventType, el._debounceHandler)
  },
  
  unmounted(el: HTMLInputElement) {
    const eventType = binding?.arg || 'input'
    el.removeEventListener(eventType, el._debounceHandler)
    delete el._debounceHandler
  }
}

可配置版本

interface DebounceBinding {
  value: (...args: any[]) => void
  arg?: 'input' | 'change' | 'keyup'
  modifiers?: {
    [key: string]: boolean  // 使用修饰符指定延迟时间
  }
}

export const vDebounce = {
  mounted(el: HTMLInputElement, binding: DebounceBinding) {
    const eventType = binding.arg || 'input'
    
    // 从修饰符中获取延迟时间,如 v-debounce:input.500
    let delay = 300 // 默认
    for (const mod in binding.modifiers) {
      const num = parseInt(mod)
      if (!isNaN(num)) {
        delay = num
        break
      }
    }
    
    el._debounceHandler = (event: Event) => {
      if (el._debounceTimer) clearTimeout(el._debounceTimer)
      
      el._debounceTimer = setTimeout(() => {
        binding.value(event)
        el._debounceTimer = null
      }, delay)
    }
    
    el.addEventListener(eventType, el._debounceHandler)
  },
  
  unmounted(el: HTMLInputElement) {
    const eventType = binding?.arg || 'input'
    el.removeEventListener(eventType, el._debounceHandler)
    if (el._debounceTimer) {
      clearTimeout(el._debounceTimer)
    }
  }
}

使用

<template>
  <input 
    v-debounce:input.500="handleSearch" 
    placeholder="搜索..."
  />
</template>

<script setup>
function handleSearch(event) {
  console.log('搜索:', event.target.value)
  // 调用 API
}
</script>

v-permission:权限控制

根据用户权限显示/隐藏元素:

// permission.ts
import { useUserStore } from '@/stores/user'

export const vPermission = {
  mounted(el: HTMLElement, binding: { 
    value: string | string[],  // 需要的权限
    modifiers?: {
      and?: boolean  // 需要同时满足多个权限
    }
  }) {
    const userStore = useUserStore()
    const permissions = userStore.permissions || []
    
    const requiredPermissions = Array.isArray(binding.value) 
      ? binding.value 
      : [binding.value]
    
    let hasPermission = false
    
    if (binding.modifiers?.and) {
      // 需要同时拥有所有权限
      hasPermission = requiredPermissions.every(p => permissions.includes(p))
    } else {
      // 拥有任意一个权限即可
      hasPermission = requiredPermissions.some(p => permissions.includes(p))
    }
    
    if (!hasPermission) {
      el.style.display = 'none'
      // 或者移除元素
      // el.parentNode?.removeChild(el)
    }
  },
  
  // 当权限更新时重新检查(如用户切换角色)
  updated(el: HTMLElement, binding: { value: string | string[] }) {
    const userStore = useUserStore()
    const permissions = userStore.permissions || []
    
    const requiredPermissions = Array.isArray(binding.value) 
      ? binding.value 
      : [binding.value]
    
    const hasPermission = requiredPermissions.some(p => permissions.includes(p))
    
    if (!hasPermission) {
      el.style.display = 'none'
    } else {
      el.style.display = ''
    }
  }
}

使用

<template>
  <!-- 需要 admin 权限 -->
  <button v-permission="'admin'">管理用户</button>
  
  <!-- 需要 admin 或 manager 权限 -->
  <button v-permission="['admin', 'manager']">高级操作</button>
  
  <!-- 需要同时拥有 admin 和 finance 权限 -->
  <button v-permission:and="['admin', 'finance']">财务操作</button>
</template>

v-lazy:图片懒加载

图片懒加载是性能优化的常用手段:

// lazy.ts
export const vLazy = {
  mounted(el: HTMLImageElement, binding: { value: string }) {
    // 保存原始图片地址
    el.dataset.src = binding.value
    
    // 创建 IntersectionObserver
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          // 图片进入视口,加载真实图片
          el.src = el.dataset.src!
          observer.unobserve(el)
          
          // 图片加载完成后添加淡入效果
          el.classList.add('loaded')
        }
      })
    }, {
      rootMargin: '50px' // 提前50px加载
    })
    
    observer.observe(el)
    
    // 保存 observer 以便清理
    el._lazyObserver = observer
  },
  
  unmounted(el: HTMLImageElement) {
    if (el._lazyObserver) {
      el._lazyObserver.unobserve(el)
      el._lazyObserver.disconnect()
    }
  }
}

增强版:支持加载中、加载失败占位

interface LazyBinding {
  value: string          // 真实图片地址
  arg?: string           // 加载中占位图
  modifiers?: {
    error?: string       // 加载失败占位图
  }
}

export const vLazy = {
  mounted(el: HTMLImageElement, binding: LazyBinding) {
    // 设置加载中占位图
    if (binding.arg) {
      el.src = binding.arg
    }
    
    // 处理加载失败
    el.onerror = () => {
      if (binding.modifiers?.error) {
        el.src = binding.modifiers.error
      }
    }
    
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          // 创建新图片对象预加载
          const img = new Image()
          img.src = binding.value
          
          img.onload = () => {
            el.src = binding.value
            el.classList.add('fade-in')
          }
          
          observer.unobserve(el)
        }
      })
    }, {
      rootMargin: '50px'
    })
    
    observer.observe(el)
    el._lazyObserver = observer
  },
  
  unmounted(el: HTMLImageElement) {
    if (el._lazyObserver) {
      el._lazyObserver.unobserve(el)
      el._lazyObserver.disconnect()
    }
  }
}

使用

<template>
  <img 
    v-lazy:loading.gif.error="'error.png'" 
    :data-src="imageUrl"
    alt="懒加载图片"
  />
</template>

<style>
img.fade-in {
  animation: fadeIn 0.3s ease-in;
}

@keyframes fadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}
</style>

指令的参数修饰符和值

使用 binding.arg 传递参数

参数让指令更加灵活:

// 示例:滚动指令
app.directive('scroll', {
  mounted(el, binding) {
    const handler = () => {
      // 根据参数执行不同逻辑
      switch (binding.arg) {
        case 'bottom':
          if (el.scrollTop + el.clientHeight >= el.scrollHeight) {
            binding.value()
          }
          break
        case 'top':
          if (el.scrollTop === 0) {
            binding.value()
          }
          break
        case 'direction':
          // 检测滚动方向
          break
      }
    }
    
    el.addEventListener('scroll', handler)
    el._scrollHandler = handler
  },
  
  unmounted(el) {
    el.removeEventListener('scroll', el._scrollHandler)
  }
})

使用:

<template>
  <div 
    v-scroll:bottom="loadMore"
    v-scroll:top="refresh"
    class="scroll-container"
  >
    <!-- 内容 -->
  </div>
</template>

使用 binding.modifiers 处理修饰符

修饰符是布尔值,适合开关型配置:

// 拖拽指令
app.directive('draggable', {
  mounted(el, binding) {
    el.style.position = 'absolute'
    el.style.cursor = 'move'
    
    const handlers = {
      mousedown: (e: MouseEvent) => {
        e.preventDefault()
        
        const startX = e.clientX - el.offsetLeft
        const startY = e.clientY - el.offsetTop
        
        const onMouseMove = (e: MouseEvent) => {
          // 根据修饰符限制移动方向
          if (!binding.modifiers?.horizontal) {
            el.style.top = (e.clientY - startY) + 'px'
          }
          if (!binding.modifiers?.vertical) {
            el.style.left = (e.clientX - startX) + 'px'
          }
          
          // 边界限制
          if (binding.modifiers?.boundary) {
            const parent = el.parentElement
            if (parent) {
              const left = parseInt(el.style.left)
              const top = parseInt(el.style.top)
              
              el.style.left = Math.max(0, Math.min(left, parent.clientWidth - el.clientWidth)) + 'px'
              el.style.top = Math.max(0, Math.min(top, parent.clientHeight - el.clientHeight)) + 'px'
            }
          }
        }
        
        const onMouseUp = () => {
          document.removeEventListener('mousemove', onMouseMove)
          document.removeEventListener('mouseup', onMouseUp)
        }
        
        document.addEventListener('mousemove', onMouseMove)
        document.addEventListener('mouseup', onMouseUp)
      }
    }
    
    el.addEventListener('mousedown', handlers.mousedown)
    el._dragHandlers = handlers
  },
  
  unmounted(el) {
    el.removeEventListener('mousedown', el._dragHandlers.mousedown)
  }
})

使用:

<template>
  <!-- 只能水平移动 -->
  <div v-draggable.horizontal class="draggable">水平拖拽</div>
  
  <!-- 只能垂直移动 -->
  <div v-draggable.vertical class="draggable">垂直拖拽</div>
  
  <!-- 边界限制 -->
  <div v-draggable.boundary class="draggable">带边界</div>
  
  <!-- 自由拖拽 -->
  <div v-draggable class="draggable">自由拖拽</div>
</template>

动态更新指令的逻辑

当指令的值变化时,可以在 updated 钩子中响应:

// 颜色指令
app.directive('color', {
  mounted(el, binding) {
    el.style.color = binding.value
  },
  
  updated(el, binding) {
    // 当值变化时更新颜色
    el.style.color = binding.value
  }
})

使用动态值:

<template>
  <div v-color="color">颜色会变化</div>
  <button @click="color = 'red'">红色</button>
  <button @click="color = 'blue'">蓝色</button>
</template>

<script setup>
const color = ref('black')
</script>

在 Composition API 中使用指令

使用 v-bind 动态绑定指令

可以通过 v-bind 动态决定是否应用指令:

<template>
  <input 
    v-bind="directives"
    v-model="searchText"
  />
</template>

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

const isDisabled = ref(false)

const directives = computed(() => {
  const dirs = {}
  
  // 根据条件添加指令
  if (!isDisabled.value) {
    dirs.focus = {}  // 应用 v-focus 指令
  }
  
  dirs.debounce = {
    value: handleSearch,
    arg: 'input',
    modifiers: { 500: true }
  }
  
  return dirs
})
</script>

组合式函数中封装指令逻辑

有时我们需要在组合式函数中封装指令相关的逻辑:

// composables/useDraggable.ts
import { ref, onMounted, onUnmounted } from 'vue'

export function useDraggable(options: {
  horizontal?: boolean
  vertical?: boolean
  boundary?: boolean
} = {}) {
  const element = ref<HTMLElement | null>(null)
  
  const startDrag = (e: MouseEvent) => {
    if (!element.value) return
    
    e.preventDefault()
    
    const el = element.value
    const startX = e.clientX - el.offsetLeft
    const startY = e.clientY - el.offsetTop
    
    const onMouseMove = (e: MouseEvent) => {
      if (!options.horizontal) {
        el.style.top = (e.clientY - startY) + 'px'
      }
      if (!options.vertical) {
        el.style.left = (e.clientX - startX) + 'px'
      }
      
      if (options.boundary && el.parentElement) {
        const parent = el.parentElement
        const left = parseInt(el.style.left)
        const top = parseInt(el.style.top)
        
        el.style.left = Math.max(0, Math.min(left, parent.clientWidth - el.clientWidth)) + 'px'
        el.style.top = Math.max(0, Math.min(top, parent.clientHeight - el.clientHeight)) + 'px'
      }
    }
    
    const onMouseUp = () => {
      document.removeEventListener('mousemove', onMouseMove)
      document.removeEventListener('mouseup', onMouseUp)
    }
    
    document.addEventListener('mousemove', onMouseMove)
    document.addEventListener('mouseup', onMouseUp)
  }
  
  onMounted(() => {
    if (element.value) {
      element.value.addEventListener('mousedown', startDrag)
    }
  })
  
  onUnmounted(() => {
    if (element.value) {
      element.value.removeEventListener('mousedown', startDrag)
    }
  })
  
  return {
    element,
    // 可以返回一个指令对象
    draggable: {
      mounted(el: HTMLElement) {
        element.value = el
        el.style.position = 'absolute'
        el.style.cursor = 'move'
      }
    }
  }
}

在组件中使用:

<template>
  <div v-draggable class="box">拖拽我</div>
</template>

<script setup>
import { useDraggable } from './composables/useDraggable'

// 方式1:直接使用指令
const { draggable } = useDraggable({ horizontal: true })

// 方式2:使用组合式函数控制
const { element } = useDraggable({ boundary: true })
</script>

TypeScript 类型支持

为自定义指令定义类型

// directives/types.ts
import { Directive } from 'vue'

// 权限指令的类型
export interface PermissionDirective {
  (el: HTMLElement, binding: {
    value: string | string[]      // 权限列表
    modifiers?: {
      and?: boolean                // 是否需要同时满足
    }
  }): void
}

// 防抖指令的类型
export interface DebounceDirective {
  (el: HTMLElement, binding: {
    value: (event: Event) => void  // 回调函数
    arg?: 'input' | 'change'       // 事件类型
    modifiers?: {                   // 延迟时间,如 v-debounce.500
      [key: string]: boolean
    }
  }): void
}

// 定义指令对象类型
export type AppDirectives = {
  'permission': Directive<HTMLElement, string | string[]>
  'debounce': Directive<HTMLElement, (event: Event) => void>
  'click-outside': Directive<HTMLElement, () => void>
  'focus': Directive<HTMLElement, number | undefined>
  'lazy': Directive<HTMLImageElement, string>
}

扩展 Vue 的类型声明

为了让 TypeScript 识别自定义指令,需要扩展 Vue 的类型:

// directives/index.ts
import { App } from 'vue'
import { vFocus } from './focus'
import { vClickOutside } from './click-outside'
import { vDebounce } from './debounce'
import { vPermission } from './permission'
import { vLazy } from './lazy'

export function setupDirectives(app: App) {
  app.directive('focus', vFocus)
  app.directive('click-outside', vClickOutside)
  app.directive('debounce', vDebounce)
  app.directive('permission', vPermission)
  app.directive('lazy', vLazy)
}

// types/vue.d.ts
import { Directive } from 'vue'

declare module '@vue/runtime-core' {
  export interface ComponentCustomProperties {
    // 如果有全局属性可以添加
  }
  
  // 扩展全局指令类型
  export interface DirectiveBinding {
    // 可以添加自定义属性
  }
}

// 为导入的指令提供类型
declare module '@/directives' {
  export const vFocus: Directive<HTMLElement, number>
  export const vClickOutside: Directive<HTMLElement, () => void>
  export const vDebounce: Directive<HTMLElement, (event: Event) => void>
  export const vPermission: Directive<HTMLElement, string | string[]>
  export const vLazy: Directive<HTMLImageElement, string>
}

在组件中局部注册并获取类型

<!-- 局部注册指令并获取类型支持 -->
<script setup lang="ts">
import { vFocus } from '@/directives/focus'
import { vDebounce } from '@/directives/debounce'

// 局部注册
defineProps<{
  modelValue: string
}>()

defineEmits<{
  'update:modelValue': [value: string]
}>()

// vDebounce 现在有类型提示了
function handleSearch(event: Event) {
  const value = (event.target as HTMLInputElement).value
  // ...
}
</script>

<template>
  <input 
    v-focus
    v-debounce:input.300="handleSearch"
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)"
  />
</template>

自定义指令的设计模式

自定义指令的使用决策树

graph TD
    A[遇到 DOM 操作需求] --> B{逻辑需要复用?}
    B -->|否| C[写在组件内即可]
    B -->|是| D{需要参数配置?}
    
    D -->|否| E[简单指令: 只用 mounted]
    D -->|是| F{需要动态更新?}
    
    F -->|否| G[使用 mounted + 参数]
    F -->|是| H[使用 mounted + updated]
    
    H --> I{需要清理资源?}
    I -->|是| J[添加 unmounted 钩子]

指令设计的最佳实践清单

  • 单一职责:一个指令只做一件事
  • 命名清晰:如 v-focusv-click-outside
  • 提供默认行为:在无参数时也能工作
  • 及时清理:在 unmounted 中移除事件
  • 使用 TypeScript:提供完整的类型定义
  • 考虑边界情况:空值、异常情况处理
  • 性能优化:避免不必要的 DOM 操作

指令模板

import { Directive } from 'vue'

// 定义参数类型
interface DirectiveBindingType {
  value: string      // 主要值
  arg?: 'option1' | 'option2'  // 参数
  modifiers?: {      // 修饰符
    [key: string]: boolean
  }
}

// 指令实现
const myDirective: Directive<HTMLElement, DirectiveBindingType> = {
  created(el, binding, vnode) {
    // 初始化
  },
  
  mounted(el, binding) {
    // DOM 已挂载,执行主要逻辑
    
    // 保存处理函数以便清理
    el._handler = (event: Event) => {
      // 处理逻辑
    }
    
    el.addEventListener('click', el._handler)
  },
  
  updated(el, binding) {
    // 当 binding.value 变化时更新
  },
  
  unmounted(el) {
    // 清理
    el.removeEventListener('click', el._handler)
    delete el._handler
  }
}

export default myDirective

最终建议

自定义指令是 Vue 提供的一个强大但容易被忽视的特性。它最适合:

  1. 原生 DOM 操作:需要直接访问 DOM 的场景
  2. 跨组件逻辑复用:多个组件共享的 DOM 相关逻辑
  3. 声明式 API:让模板更加声明式、可读性更好

结语

当我们在组件中频繁使用 ref 配合生命周期钩子操作 DOM 时,不妨考虑一下,是否应该将其封装成自定义指令。这不仅能让组件代码更加简洁,还能让这些逻辑在项目中被轻松复用。

对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:alixiixcom@163.com