Vue 自定义指令生命周期钩子完全指南

Vue 自定义指令生命周期钩子完全指南

Vue 自定义指令提供了强大的生命周期钩子,让你可以精准控制指令在 DOM 元素上的行为。本文将深入解析所有钩子函数,并提供丰富的实用示例!

一、指令生命周期概览

1.1 生命周期钩子总览

javascript 复制代码
const myDirective = {
  // 1. 绑定前(Vue 3 新增)
  beforeMount() {},
  
  // 2. 元素挂载时
  mounted() {},
  
  // 3. 更新前(Vue 2: bind → Vue 3: beforeUpdate)
  beforeUpdate() {},
  
  // 4. 更新后
  updated() {},
  
  // 5. 卸载前(Vue 2: unbind → Vue 3: beforeUnmount)
  beforeUnmount() {},
  
  // 6. 卸载后(Vue 2: unbind → Vue 3: unmounted)
  unmounted() {},
  
  // 7. Vue 2 特有
  bind() {},      // Vue 3 中被 beforeMount + mounted 替代
  inserted() {},  // Vue 3 中被 mounted 替代
  componentUpdated() {}, // Vue 3 中被 updated 替代
  unbind() {}     // Vue 3 中被 beforeUnmount + unmounted 替代
}

1.2 Vue 2 vs Vue 3 对比

javascript 复制代码
// Vue 2 指令生命周期
const vue2Directive = {
  bind(el, binding, vnode, oldVnode) {
    // 只调用一次,指令第一次绑定到元素时调用
  },
  inserted(el, binding, vnode, oldVnode) {
    // 被绑定元素插入父节点时调用
  },
  update(el, binding, vnode, oldVnode) {
    // 所在组件的 VNode 更新时调用
  },
  componentUpdated(el, binding, vnode, oldVnode) {
    // 指令所在组件的 VNode 及其子 VNode 全部更新后调用
  },
  unbind(el, binding, vnode, oldVnode) {
    // 只调用一次,指令与元素解绑时调用
  }
}

// Vue 3 指令生命周期(组合式API风格)
const vue3Directive = {
  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.1 钩子参数解析

javascript 复制代码
const directive = {
  mounted(el, binding, vnode, prevVnode) {
    // 参数详解:
    
    // 1. el - 指令绑定的元素
    console.log('元素:', el)  // DOM 元素
    
    // 2. binding - 包含指令信息的对象
    console.log('binding 对象:', {
      // 指令的值(v-my-directive="value" 中的 value)
      value: binding.value,
      
      // 旧值(仅在 beforeUpdate 和 updated 中可用)
      oldValue: binding.oldValue,
      
      // 参数(v-my-directive:arg 中的 arg)
      arg: binding.arg,
      
      // 修饰符对象(v-my-directive.modifier 中的 modifier)
      modifiers: binding.modifiers,
      
      // 指令的实例
      instance: binding.instance,
      
      // 指令的定义对象
      dir: binding.dir
    })
    
    // 3. vnode - 绑定元素的虚拟节点
    console.log('虚拟节点:', {
      type: vnode.type,
      props: vnode.props,
      children: vnode.children,
      el: vnode.el,  // 对应的 DOM 元素
      component: vnode.component,  // 组件实例
      dirs: vnode.dirs  // 指令数组
    })
    
    // 4. prevVnode - 先前的虚拟节点(仅在更新钩子中可用)
    console.log('先前虚拟节点:', prevVnode)
  }
}

2.2 钩子执行时机详解

vue 复制代码
<template>
  <div>
    <!-- 指令生命周期演示 -->
    <div 
      v-lifecycle-demo="counter" 
      v-if="showElement"
      :class="{ active: isActive }"
    >
      指令生命周期演示
    </div>
    
    <button @click="increment">增加: {{ counter }}</button>
    <button @click="toggleElement">切换显示</button>
    <button @click="toggleClass">切换类名</button>
  </div>
</template>

<script>
// 生命周期演示指令
const lifecycleDemo = {
  beforeMount(el, binding) {
    console.log('1. beforeMount - 绑定前', {
      value: binding.value,
      elementExists: !!el.parentNode
    })
  },
  
  mounted(el, binding, vnode) {
    console.log('2. mounted - 挂载完成', {
      value: binding.value,
      elementInDOM: document.body.contains(el),
      parent: el.parentNode?.tagName
    })
    
    // 添加初始样式
    el.style.transition = 'all 0.3s ease'
  },
  
  beforeUpdate(el, binding) {
    console.log('3. beforeUpdate - 更新前', {
      oldValue: binding.oldValue,
      newValue: binding.value,
      willUpdate: binding.value !== binding.oldValue
    })
  },
  
  updated(el, binding) {
    console.log('4. updated - 更新完成', {
      oldValue: binding.oldValue,
      newValue: binding.value,
      elementText: el.textContent
    })
    
    // 根据值变化添加动画
    if (binding.value > binding.oldValue) {
      el.style.transform = 'scale(1.1)'
      setTimeout(() => {
        el.style.transform = 'scale(1)'
      }, 300)
    }
  },
  
  beforeUnmount(el, binding) {
    console.log('5. beforeUnmount - 卸载前', {
      value: binding.value,
      elementInDOM: document.body.contains(el)
    })
    
    // 添加淡出动画
    el.style.opacity = '0.5'
  },
  
  unmounted(el, binding) {
    console.log('6. unmounted - 卸载完成', {
      value: binding.value,
      elementInDOM: false,  // 此时元素已从DOM移除
      elementReference: el  // el 仍然可以访问,但已不在DOM中
    })
  }
}

export default {
  directives: {
    'lifecycle-demo': lifecycleDemo
  },
  data() {
    return {
      counter: 0,
      showElement: true,
      isActive: false
    }
  },
  methods: {
    increment() {
      this.counter++
    },
    toggleElement() {
      this.showElement = !this.showElement
    },
    toggleClass() {
      this.isActive = !this.isActive
    }
  },
  mounted() {
    console.log('组件 mounted - 开始观察指令生命周期')
  }
}
</script>

三、实用指令示例

3.1 焦点管理指令

javascript 复制代码
// 自动聚焦指令
const vFocus = {
  mounted(el, binding) {
    const { value = true, arg = 'auto', modifiers } = binding
    
    if (value) {
      // 立即聚焦
      if (arg === 'immediate') {
        el.focus()
      }
      // 延迟聚焦
      else if (arg === 'delay') {
        setTimeout(() => {
          el.focus()
        }, binding.value.delay || 100)
      }
      // 条件聚焦
      else if (arg === 'conditional') {
        if (binding.value.condition) {
          el.focus()
        }
      }
      // 自动聚焦(默认)
      else {
        // 对于输入框,自动聚焦
        if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
          el.focus()
        }
      }
      
      // 处理修饰符
      if (modifiers.select) {
        el.select()
      }
      
      if (modifiers.end) {
        el.setSelectionRange(el.value.length, el.value.length)
      }
    }
  },
  
  updated(el, binding) {
    // 值变化时重新聚焦
    if (binding.value !== binding.oldValue && binding.value) {
      el.focus()
      
      if (binding.modifiers.select) {
        el.select()
      }
    }
  },
  
  beforeUnmount(el) {
    // 卸载前移除焦点
    el.blur()
  }
}

// 用法示例
// <input v-focus>                    // 自动聚焦
// <input v-focus.immediate>         // 立即聚焦
// <input v-focus:delay="{delay: 500}"> // 延迟500ms聚焦
// <input v-focus:conditional="{condition: shouldFocus}"> // 条件聚焦
// <input v-focus.select>            // 聚焦并选中文本
// <input v-focus.end>               // 聚焦到末尾

3.2 点击外部指令

javascript 复制代码
// 点击外部关闭指令
const vClickOutside = {
  beforeMount(el, binding) {
    // 创建事件处理函数
    el._clickOutsideHandler = function(event) {
      // 检查点击是否在元素外部
      if (!(el === event.target || el.contains(event.target))) {
        // 调用绑定函数
        binding.value(event)
      }
    }
    
    // 添加事件监听
    document.addEventListener('click', el._clickOutsideHandler)
    
    // 可选:添加其他事件类型
    if (binding.modifiers.mousedown) {
      document.addEventListener('mousedown', el._clickOutsideHandler)
    }
    
    if (binding.modifiers.touchstart) {
      document.addEventListener('touchstart', el._clickOutsideHandler)
    }
  },
  
  updated(el, binding) {
    // 更新时检查值是否变化
    if (binding.value !== binding.oldValue) {
      // 可以在这里更新处理逻辑
      console.log('点击外部指令值已更新')
    }
  },
  
  unmounted(el, binding) {
    // 移除事件监听
    document.removeEventListener('click', el._clickOutsideHandler)
    
    if (binding.modifiers.mousedown) {
      document.removeEventListener('mousedown', el._clickOutsideHandler)
    }
    
    if (binding.modifiers.touchstart) {
      document.removeEventListener('touchstart', el._clickOutsideHandler)
    }
    
    // 清理引用
    delete el._clickOutsideHandler
  }
}

// 高级版本:支持配置和动态启用/禁用
const vClickOutsideAdvanced = {
  mounted(el, binding) {
    const { value, modifiers, arg } = binding
    
    // 默认配置
    const defaultConfig = {
      handler: value,
      events: ['click'],
      enabled: true,
      capture: false,
      immediate: false
    }
    
    // 合并配置
    const config = typeof value === 'function' 
      ? { ...defaultConfig, handler: value }
      : { ...defaultConfig, ...value }
    
    // 处理修饰符
    if (modifiers.mousedown) config.events.push('mousedown')
    if (modifiers.touchstart) config.events.push('touchstart')
    if (modifiers.capture) config.capture = true
    if (modifiers.immediate) config.immediate = true
    
    // 处理参数
    if (arg === 'except') {
      // 排除某些元素
      config.except = binding.value?.except || []
    }
    
    // 存储配置
    el._clickOutsideConfig = config
    
    // 事件处理函数
    el._clickOutsideHandler = (event) => {
      if (!config.enabled) return
      
      // 检查点击是否在排除列表中
      if (config.except) {
        const clickedInsideExcept = config.except.some(selector => {
          const elements = document.querySelectorAll(selector)
          return Array.from(elements).some(element => 
            element.contains(event.target)
          )
        })
        
        if (clickedInsideExcept) return
      }
      
      // 检查点击是否在元素外部
      if (!(el === event.target || el.contains(event.target))) {
        config.handler(event, el)
      }
    }
    
    // 添加事件监听
    config.events.forEach(eventName => {
      document.addEventListener(
        eventName, 
        el._clickOutsideHandler, 
        config.capture
      )
    })
    
    // 立即执行一次检查(如果配置了)
    if (config.immediate) {
      // 模拟外部点击
      setTimeout(() => {
        const fakeEvent = new MouseEvent('click')
        el._clickOutsideHandler(fakeEvent)
      })
    }
  },
  
  updated(el, binding) {
    const oldConfig = el._clickOutsideConfig
    const newValue = binding.value
    
    // 更新配置
    if (typeof newValue === 'function') {
      el._clickOutsideConfig.handler = newValue
    } else if (typeof newValue === 'object') {
      Object.assign(el._clickOutsideConfig, newValue)
    }
    
    // 如果启停状态变化,可能需要更新事件监听
    if (oldConfig?.enabled !== el._clickOutsideConfig?.enabled) {
      console.log('点击外部指令启停状态变化')
    }
  },
  
  beforeUnmount(el) {
    const config = el._clickOutsideConfig
    
    if (config && el._clickOutsideHandler) {
      // 移除所有事件监听
      config.events.forEach(eventName => {
        document.removeEventListener(
          eventName, 
          el._clickOutsideHandler, 
          config.capture
        )
      })
      
      // 清理引用
      delete el._clickOutsideConfig
      delete el._clickOutsideHandler
    }
  }
}

// 用法示例
// <div v-click-outside="closeMenu">菜单内容</div>
// <div v-click-outside.advanced="{ handler: closeMenu, enabled: isOpen }">
// <div v-click-outside:except="{ handler: closeMenu, except: ['.ignore-click'] }">
// <div v-click-outside.mousedown.touchstart="closeMenu">

3.3 滚动指令

javascript 复制代码
// 滚动指令集
const scrollDirectives = {
  // 无限滚动指令
  infiniteScroll: {
    mounted(el, binding) {
      const { value: callback, modifiers, arg } = binding
      
      // 配置选项
      const options = {
        distance: 50,           // 触发距离
        delay: 100,            // 防抖延迟
        immediate: true,       // 立即检查
        disabled: false,       // 是否禁用
        direction: 'vertical'  // 滚动方向
      }
      
      // 解析参数
      if (arg === 'distance') {
        options.distance = parseInt(binding.value) || options.distance
      } else if (typeof binding.value === 'object') {
        Object.assign(options, binding.value)
      }
      
      // 存储配置
      el._infiniteScrollOptions = options
      
      // 防抖函数
      let checkTimer = null
      const checkScroll = () => {
        if (options.disabled) return
        
        let isAtEnd = false
        
        if (options.direction === 'vertical') {
          const scrollTop = el.scrollTop
          const scrollHeight = el.scrollHeight
          const clientHeight = el.clientHeight
          
          isAtEnd = scrollHeight - scrollTop - clientHeight <= options.distance
        } else {
          const scrollLeft = el.scrollLeft
          const scrollWidth = el.scrollWidth
          const clientWidth = el.clientWidth
          
          isAtEnd = scrollWidth - scrollLeft - clientWidth <= options.distance
        }
        
        if (isAtEnd) {
          callback()
        }
      }
      
      const debouncedCheck = () => {
        if (checkTimer) clearTimeout(checkTimer)
        checkTimer = setTimeout(checkScroll, options.delay)
      }
      
      // 监听滚动事件
      el._infiniteScrollHandler = debouncedCheck
      el.addEventListener('scroll', el._infiniteScrollHandler)
      
      // 监听窗口大小变化
      el._resizeHandler = debouncedCheck
      window.addEventListener('resize', el._resizeHandler)
      
      // 立即检查一次
      if (options.immediate) {
        setTimeout(checkScroll, 100)
      }
    },
    
    updated(el, binding) {
      const options = el._infiniteScrollOptions
      const newValue = binding.value
      
      // 更新配置
      if (typeof newValue === 'object') {
        Object.assign(options, newValue)
      } else if (typeof newValue === 'function') {
        // 如果传入了新函数,需要更新回调
        // 注意:这里需要重新绑定事件,简化处理
      }
      
      // 强制检查一次
      if (binding.modifiers.force) {
        setTimeout(() => {
          const event = new Event('scroll')
          el.dispatchEvent(event)
        })
      }
    },
    
    beforeUnmount(el) {
      // 清理事件监听
      if (el._infiniteScrollHandler) {
        el.removeEventListener('scroll', el._infiniteScrollHandler)
        delete el._infiniteScrollHandler
      }
      
      if (el._resizeHandler) {
        window.removeEventListener('resize', el._resizeHandler)
        delete el._resizeHandler
      }
      
      delete el._infiniteScrollOptions
    }
  },
  
  // 滚动到元素指令
  scrollTo: {
    mounted(el, binding) {
      const { value, modifiers, arg } = binding
      
      el._scrollToHandler = (event) => {
        event.preventDefault()
        
        const targetSelector = typeof value === 'string' ? value : value?.target
        const options = typeof value === 'object' ? value : {}
        
        const targetElement = targetSelector 
          ? document.querySelector(targetSelector)
          : document.documentElement
        
        if (!targetElement) return
        
        // 滚动配置
        const scrollOptions = {
          behavior: modifiers.smooth ? 'smooth' : 'auto',
          block: arg || 'start',  // start, center, end, nearest
          inline: 'nearest'
        }
        
        // 合并选项
        Object.assign(scrollOptions, options)
        
        // 执行滚动
        targetElement.scrollIntoView(scrollOptions)
        
        // 触发回调
        if (typeof value === 'object' && value.onScroll) {
          value.onScroll(targetElement)
        }
      }
      
      // 添加点击事件
      el.addEventListener('click', el._scrollToHandler)
      
      // 自动滚动(如果配置了)
      if (modifiers.auto) {
        setTimeout(() => {
          el._scrollToHandler(new Event('click'))
        }, value?.delay || 0)
      }
    },
    
    updated(el, binding) {
      // 如果值变化且配置了auto,重新触发
      if (binding.value !== binding.oldValue && binding.modifiers.auto) {
        setTimeout(() => {
          if (el._scrollToHandler) {
            el._scrollToHandler(new Event('click'))
          }
        }, binding.value?.delay || 0)
      }
    },
    
    beforeUnmount(el) {
      if (el._scrollToHandler) {
        el.removeEventListener('click', el._scrollToHandler)
        delete el._scrollToHandler
      }
    }
  }
}

// 用法示例
// <div v-infinite-scroll="loadMore">内容...</div>
// <div v-infinite-scroll.distance="100">自定义距离</div>
// <div v-infinite-scroll="{ handler: loadMore, distance: 100, disabled: isLoading }">
// <button v-scroll-to="'#section'">滚动到章节</button>
// <button v-scroll-to.smooth.auto="{ target: '#section', delay: 500 }">自动滚动</button>

3.4 拖放指令

javascript 复制代码
// 拖放指令
const vDrag = {
  beforeMount(el, binding) {
    const { value, modifiers } = binding
    
    // 默认配置
    const config = {
      data: null,           // 拖拽数据
      effect: 'move',       // 拖拽效果
      disabled: false,      // 是否禁用
      handle: null,         // 拖拽手柄选择器
      ghost: true,          // 显示幽灵图像
      clone: false,         // 克隆元素
      axis: 'both',         // 拖拽轴向:both, x, y
      boundary: null,       // 边界限制
      onStart: null,        // 开始回调
      onMove: null,         // 移动回调
      onEnd: null           // 结束回调
    }
    
    // 合并配置
    if (typeof value === 'object') {
      Object.assign(config, value)
    } else if (value !== undefined) {
      config.data = value
    }
    
    // 处理修饰符
    if (modifiers.copy) config.effect = 'copy'
    if (modifiers.link) config.effect = 'link'
    if (modifiers.x) config.axis = 'x'
    if (modifiers.y) config.axis = 'y'
    if (modifiers.noGhost) config.ghost = false
    if (modifiers.clone) config.clone = true
    
    // 存储配置和状态
    el._dragConfig = config
    el._dragState = {
      isDragging: false,
      startX: 0,
      startY: 0,
      offsetX: 0,
      offsetY: 0,
      clone: null
    }
    
    // 设置元素属性
    el.setAttribute('draggable', !config.disabled)
    
    // 找到拖拽手柄
    const dragHandle = config.handle 
      ? el.querySelector(config.handle) 
      : el
    
    // 事件处理函数
    const onDragStart = (e) => {
      if (config.disabled) {
        e.preventDefault()
        return
      }
      
      el._dragState.isDragging = true
      el._dragState.startX = e.clientX
      el._dragState.startY = e.clientY
      
      // 设置拖拽数据
      if (config.data !== null) {
        const dataString = typeof config.data === 'string' 
          ? config.data 
          : JSON.stringify(config.data)
        
        e.dataTransfer.setData('application/json', dataString)
        e.dataTransfer.setData('text/plain', dataString)
      }
      
      // 设置拖拽效果
      e.dataTransfer.effectAllowed = config.effect
      
      // 创建幽灵图像
      if (config.ghost) {
        const ghost = el.cloneNode(true)
        ghost.style.opacity = '0.5'
        ghost.style.position = 'absolute'
        ghost.style.top = '-1000px'
        document.body.appendChild(ghost)
        e.dataTransfer.setDragImage(ghost, 0, 0)
        
        // 稍后移除
        setTimeout(() => {
          if (document.body.contains(ghost)) {
            document.body.removeChild(ghost)
          }
        }, 0)
      }
      
      // 克隆元素(如果需要)
      if (config.clone) {
        const clone = el.cloneNode(true)
        clone.style.position = 'absolute'
        clone.style.zIndex = '1000'
        clone.style.pointerEvents = 'none'
        clone.style.opacity = '0.7'
        document.body.appendChild(clone)
        el._dragState.clone = clone
      }
      
      // 触发开始回调
      if (typeof config.onStart === 'function') {
        config.onStart(e, el, config.data)
      }
      
      // 添加拖拽样式
      el.classList.add('dragging')
    }
    
    const onDrag = (e) => {
      if (!el._dragState.isDragging) return
      
      // 计算偏移
      const deltaX = e.clientX - el._dragState.startX
      const deltaY = e.clientY - el._dragState.startY
      
      // 轴向限制
      if (config.axis === 'x') {
        el._dragState.offsetX = deltaX
        el._dragState.offsetY = 0
      } else if (config.axis === 'y') {
        el._dragState.offsetX = 0
        el._dragState.offsetY = deltaY
      } else {
        el._dragState.offsetX = deltaX
        el._dragState.offsetY = deltaY
      }
      
      // 边界限制
      if (config.boundary) {
        const boundary = typeof config.boundary === 'string'
          ? document.querySelector(config.boundary)
          : config.boundary
        
        if (boundary) {
          const boundRect = boundary.getBoundingClientRect()
          const elRect = el.getBoundingClientRect()
          
          // 限制在边界内
          el._dragState.offsetX = Math.max(
            boundRect.left - elRect.left,
            Math.min(el._dragState.offsetX, boundRect.right - elRect.right)
          )
          
          el._dragState.offsetY = Math.max(
            boundRect.top - elRect.top,
            Math.min(el._dragState.offsetY, boundRect.bottom - elRect.bottom)
          )
        }
      }
      
      // 更新克隆元素位置
      if (el._dragState.clone) {
        el._dragState.clone.style.transform = 
          `translate(${el._dragState.offsetX}px, ${el._dragState.offsetY}px)`
      }
      
      // 触发移动回调
      if (typeof config.onMove === 'function') {
        config.onMove(e, el, {
          offsetX: el._dragState.offsetX,
          offsetY: el._dragState.offsetY,
          deltaX,
          deltaY
        })
      }
    }
    
    const onDragEnd = (e) => {
      if (!el._dragState.isDragging) return
      
      el._dragState.isDragging = false
      
      // 移除克隆元素
      if (el._dragState.clone) {
        document.body.removeChild(el._dragState.clone)
        el._dragState.clone = null
      }
      
      // 触发结束回调
      if (typeof config.onEnd === 'function') {
        config.onEnd(e, el, {
          offsetX: el._dragState.offsetX,
          offsetY: el._dragState.offsetY,
          success: e.dataTransfer.dropEffect !== 'none'
        })
      }
      
      // 重置状态
      el._dragState.offsetX = 0
      el._dragState.offsetY = 0
      
      // 移除拖拽样式
      el.classList.remove('dragging')
    }
    
    // 绑定事件
    dragHandle.addEventListener('dragstart', onDragStart)
    dragHandle.addEventListener('drag', onDrag)
    dragHandle.addEventListener('dragend', onDragEnd)
    
    // 存储事件处理函数以便清理
    el._dragHandlers = { onDragStart, onDrag, onDragEnd }
  },
  
  updated(el, binding) {
    const config = el._dragConfig
    const newValue = binding.value
    
    // 更新配置
    if (typeof newValue === 'object') {
      Object.assign(config, newValue)
    }
    
    // 更新 draggable 属性
    el.setAttribute('draggable', !config.disabled)
    
    // 如果禁用状态变化
    if (binding.oldValue?.disabled !== config.disabled) {
      console.log('拖拽指令禁用状态变化:', config.disabled)
    }
  },
  
  beforeUnmount(el) {
    const dragHandle = el._dragConfig?.handle 
      ? el.querySelector(el._dragConfig.handle) 
      : el
    
    const handlers = el._dragHandlers
    
    if (dragHandle && handlers) {
      dragHandle.removeEventListener('dragstart', handlers.onDragStart)
      dragHandle.removeEventListener('drag', handlers.onDrag)
      dragHandle.removeEventListener('dragend', handlers.onDragEnd)
    }
    
    // 清理克隆元素
    if (el._dragState?.clone && document.body.contains(el._dragState.clone)) {
      document.body.removeChild(el._dragState.clone)
    }
    
    // 清理引用
    delete el._dragConfig
    delete el._dragState
    delete el._dragHandlers
  }
}

// 用法示例
// <div v-drag="dragData">可拖拽</div>
// <div v-drag.copy>复制模式</div>
// <div v-drag.x>仅水平拖拽</div>
// <div v-drag="{ data: item, disabled: !isEditable, onEnd: handleDrop }">
// <div v-drag.clone>拖拽时显示克隆</div>
// <div v-drag.no-ghost>不显示幽灵图像</div>

3.5 权限控制指令

javascript 复制代码
// 权限控制指令集
const permissionDirectives = {
  // 角色权限指令
  role: {
    beforeMount(el, binding) {
      const { value, modifiers, arg } = binding
      
      // 获取当前用户角色
      const userRole = getCurrentUserRole()
      
      // 检查权限
      const hasPermission = checkRolePermission(userRole, value, arg)
      
      // 根据权限显示/隐藏元素
      if (!hasPermission) {
        if (modifiers.hide) {
          // 隐藏元素
          el.style.display = 'none'
          el._originalDisplay = el.style.display
        } else if (modifiers.disable) {
          // 禁用元素
          el.disabled = true
          el._originalDisabled = el.disabled
          el.classList.add('disabled')
        } else if (modifiers.remove) {
          // 移除元素
          el.parentNode?.removeChild(el)
        } else {
          // 默认:隐藏元素
          el.style.display = 'none'
          el._originalDisplay = el.style.display
        }
      }
      
      // 存储权限信息
      el._permissionInfo = {
        required: value,
        userRole,
        hasPermission,
        action: arg || 'view'
      }
    },
    
    updated(el, binding) {
      const oldPermission = el._permissionInfo
      const newValue = binding.value
      
      // 重新检查权限
      const userRole = getCurrentUserRole()
      const hasPermission = checkRolePermission(userRole, newValue, binding.arg)
      
      // 如果权限状态变化
      if (oldPermission.hasPermission !== hasPermission) {
        // 恢复原始状态
        if (oldPermission.hasPermission === false) {
          if (binding.modifiers.hide && el._originalDisplay !== undefined) {
            el.style.display = el._originalDisplay
          } else if (binding.modifiers.disable && el._originalDisabled !== undefined) {
            el.disabled = el._originalDisabled
            el.classList.remove('disabled')
          }
        }
        
        // 应用新权限
        if (!hasPermission) {
          if (binding.modifiers.hide) {
            el._originalDisplay = el.style.display
            el.style.display = 'none'
          } else if (binding.modifiers.disable) {
            el._originalDisabled = el.disabled
            el.disabled = true
            el.classList.add('disabled')
          } else if (binding.modifiers.remove) {
            el.parentNode?.removeChild(el)
          }
        }
        
        // 更新权限信息
        el._permissionInfo = {
          required: newValue,
          userRole,
          hasPermission,
          action: binding.arg || 'view'
        }
      }
    },
    
    unmounted(el) {
      // 清理
      delete el._permissionInfo
      delete el._originalDisplay
      delete el._originalDisabled
    }
  },
  
  // 功能权限指令
  feature: {
    mounted(el, binding) {
      const { value, modifiers } = binding
      
      // 检查功能是否启用
      const isEnabled = checkFeatureEnabled(value)
      
      if (!isEnabled) {
        if (modifiers.hide) {
          el.style.display = 'none'
        } else if (modifiers.disable) {
          el.disabled = true
          el.classList.add('disabled')
        } else {
          el.style.opacity = '0.5'
          el.style.pointerEvents = 'none'
        }
      }
      
      el._featureInfo = {
        feature: value,
        enabled: isEnabled
      }
    },
    
    updated(el, binding) {
      const isEnabled = checkFeatureEnabled(binding.value)
      
      if (el._featureInfo.enabled !== isEnabled) {
        if (isEnabled) {
          // 恢复
          if (binding.modifiers.hide) {
            el.style.display = ''
          } else if (binding.modifiers.disable) {
            el.disabled = false
            el.classList.remove('disabled')
          } else {
            el.style.opacity = ''
            el.style.pointerEvents = ''
          }
        } else {
          // 禁用
          if (binding.modifiers.hide) {
            el.style.display = 'none'
          } else if (binding.modifiers.disable) {
            el.disabled = true
            el.classList.add('disabled')
          } else {
            el.style.opacity = '0.5'
            el.style.pointerEvents = 'none'
          }
        }
        
        el._featureInfo = {
          feature: binding.value,
          enabled: isEnabled
        }
      }
    },
    
    unmounted(el) {
      delete el._featureInfo
    }
  }
}

// 工具函数
function getCurrentUserRole() {
  // 从Vuex、Pinia或localStorage获取
  return localStorage.getItem('userRole') || 'guest'
}

function checkRolePermission(userRole, required, action = 'view') {
  // 权限配置
  const permissions = {
    admin: ['create', 'read', 'update', 'delete', 'manage'],
    editor: ['create', 'read', 'update'],
    viewer: ['read'],
    guest: []
  }
  
  // 如果是数组,检查任意一个
  if (Array.isArray(required)) {
    return required.some(role => 
      permissions[userRole]?.includes(action) && role === userRole
    )
  }
  
  // 如果是字符串,精确匹配
  return permissions[userRole]?.includes(action) && required === userRole
}

function checkFeatureEnabled(feature) {
  // 从配置或特性开关获取
  const featureFlags = JSON.parse(localStorage.getItem('featureFlags') || '{}')
  return featureFlags[feature] !== false
}

// 用法示例
// <button v-role="'admin'">仅管理员可见</button>
// <button v-role="['admin', 'editor']">管理员和编辑可见</button>
// <button v-role:edit="'admin'">管理员可编辑</button>
// <button v-role.hide="'admin'">非管理员隐藏</button>
// <button v-role.disable="'admin'">非管理员禁用</button>
// <div v-feature="'newUI'">新UI功能</div>
// <div v-feature.disable="'betaFeature'">测试功能</div>

四、指令组合与复用

4.1 指令组合器

javascript 复制代码
// 指令组合器:将多个指令组合成一个
function createDirectiveComposer(...directives) {
  return {
    beforeMount(...args) {
      directives.forEach(directive => {
        if (directive.beforeMount) directive.beforeMount(...args)
      })
    },
    
    mounted(...args) {
      directives.forEach(directive => {
        if (directive.mounted) directive.mounted(...args)
      })
    },
    
    beforeUpdate(...args) {
      directives.forEach(directive => {
        if (directive.beforeUpdate) directive.beforeUpdate(...args)
      })
    },
    
    updated(...args) {
      directives.forEach(directive => {
        if (directive.updated) directive.updated(...args)
      })
    },
    
    beforeUnmount(...args) {
      directives.forEach(directive => {
        if (directive.beforeUnmount) directive.beforeUnmount(...args)
      })
    },
    
    unmounted(...args) {
      directives.forEach(directive => {
        if (directive.unmounted) directive.unmounted(...args)
      })
    }
  }
}

// 使用示例
const vTooltip = {
  mounted(el, binding) {
    el.title = binding.value
    el.classList.add('has-tooltip')
  },
  unmounted(el) {
    el.classList.remove('has-tooltip')
  }
}

const vHighlight = {
  mounted(el, binding) {
    if (binding.value) {
      el.classList.add('highlight')
    }
  },
  updated(el, binding) {
    if (binding.value) {
      el.classList.add('highlight')
    } else {
      el.classList.remove('highlight')
    }
  }
}

// 组合指令
const vTooltipHighlight = createDirectiveComposer(vTooltip, vHighlight)

// 注册
app.directive('tooltip-highlight', vTooltipHighlight)

// 使用
// <div v-tooltip-highlight="'提示文本'">内容</div>

4.2 指令工厂函数

javascript 复制代码
// 指令工厂:创建可配置的指令
function createResizableDirective(options = {}) {
  const defaultOptions = {
    handles: ['right', 'bottom', 'bottom-right'],
    minWidth: 100,
    minHeight: 100,
    maxWidth: null,
    maxHeight: null,
    onResize: null,
    onResizeStart: null,
    onResizeEnd: null
  }
  
  const config = { ...defaultOptions, ...options }
  
  return {
    mounted(el, binding) {
      const instanceOptions = typeof binding.value === 'object' 
        ? { ...config, ...binding.value }
        : config
      
      // 创建调整大小的手柄
      const handles = instanceOptions.handles
      const handleElements = []
      
      handles.forEach(handle => {
        const handleEl = document.createElement('div')
        handleEl.className = `resize-handle resize-handle-${handle}`
        handleEl.dataset.handle = handle
        
        // 添加事件监听
        handleEl.addEventListener('mousedown', (e) => {
          e.preventDefault()
          e.stopPropagation()
          startResize(e, handle, el, instanceOptions)
        })
        
        el.appendChild(handleEl)
        handleElements.push(handleEl)
      })
      
      // 存储引用
      el._resizeHandles = handleElements
      el._resizeOptions = instanceOptions
      
      // 添加可调整大小的样式
      el.classList.add('resizable')
    },
    
    updated(el, binding) {
      // 更新选项
      if (typeof binding.value === 'object') {
        Object.assign(el._resizeOptions, binding.value)
      }
    },
    
    beforeUnmount(el) {
      // 清理手柄
      if (el._resizeHandles) {
        el._resizeHandles.forEach(handle => {
          handle.removeEventListener('mousedown', handle._resizeHandler)
          el.removeChild(handle)
        })
        delete el._resizeHandles
      }
      
      delete el._resizeOptions
      el.classList.remove('resizable')
    }
  }
}

// 调整大小逻辑
function startResize(e, handle, el, options) {
  const startX = e.clientX
  const startY = e.clientY
  const startWidth = el.offsetWidth
  const startHeight = el.offsetHeight
  
  // 触发开始回调
  if (typeof options.onResizeStart === 'function') {
    options.onResizeStart(e, el, { width: startWidth, height: startHeight })
  }
  
  // 鼠标移动处理
  const onMouseMove = (e) => {
    const deltaX = e.clientX - startX
    const deltaY = e.clientY - startY
    
    let newWidth = startWidth
    let newHeight = startHeight
    
    // 根据手柄类型计算新尺寸
    if (handle.includes('right')) {
      newWidth = Math.max(options.minWidth, startWidth + deltaX)
      if (options.maxWidth) {
        newWidth = Math.min(options.maxWidth, newWidth)
      }
    }
    
    if (handle.includes('bottom')) {
      newHeight = Math.max(options.minHeight, startHeight + deltaY)
      if (options.maxHeight) {
        newHeight = Math.min(options.maxHeight, newHeight)
      }
    }
    
    // 应用新尺寸
    el.style.width = `${newWidth}px`
    el.style.height = `${newHeight}px`
    
    // 触发调整回调
    if (typeof options.onResize === 'function') {
      options.onResize(e, el, { width: newWidth, height: newHeight })
    }
  }
  
  // 鼠标抬起处理
  const onMouseUp = (e) => {
    document.removeEventListener('mousemove', onMouseMove)
    document.removeEventListener('mouseup', onMouseUp)
    
    // 触发结束回调
    if (typeof options.onResizeEnd === 'function') {
      options.onResizeEnd(e, el, {
        width: el.offsetWidth,
        height: el.offsetHeight
      })
    }
  }
  
  // 添加全局事件监听
  document.addEventListener('mousemove', onMouseMove)
  document.addEventListener('mouseup', onMouseUp)
}

// 创建不同配置的指令
const vResizable = createResizableDirective()
const vResizableHorizontal = createResizableDirective({ handles: ['right'] })
const vResizableVertical = createResizableDirective({ handles: ['bottom'] })

// 用法示例
// <div v-resizable>可调整大小</div>
// <div v-resizable="{ minWidth: 200, maxWidth: 800, onResize: handleResize }">
// <div v-resizable.horizontal>仅水平调整</div>

五、最佳实践与注意事项

5.1 性能优化

javascript 复制代码
// 1. 使用防抖和节流
import { debounce, throttle } from 'lodash'

const vScrollOptimized = {
  mounted(el, binding) {
    const handler = binding.value
    
    // 使用防抖
    const debouncedHandler = debounce(handler, 300, {
      leading: true,
      trailing: true
    })
    
    // 使用节流
    const throttledHandler = throttle(handler, 100)
    
    el._scrollHandler = binding.modifiers.debounce 
      ? debouncedHandler
      : binding.modifiers.throttle
        ? throttledHandler
        : handler
    
    el.addEventListener('scroll', el._scrollHandler)
  },
  
  beforeUnmount(el) {
    el.removeEventListener('scroll', el._scrollHandler)
  }
}

// 2. 事件委托
const vEventDelegation = {
  mounted(el, binding) {
    const eventType = binding.arg || 'click'
    const selector = binding.value.selector
    const handler = binding.value.handler
    
    el._delegationHandler = (e) => {
      // 检查事件目标是否匹配选择器
      if (e.target.matches(selector) || e.target.closest(selector)) {
        handler(e)
      }
    }
    
    el.addEventListener(eventType, el._delegationHandler)
  },
  
  beforeUnmount(el) {
    el.removeEventListener(binding.arg || 'click', el._delegationHandler)
  }
}

// 3. 指令复用
// 创建可复用的基础指令
const baseDirective = (customHooks = {}) => ({
  beforeMount(el, binding, vnode) {
    // 公共前置逻辑
    console.log('指令绑定到元素:', el.tagName)
    
    // 调用自定义钩子
    if (customHooks.beforeMount) {
      customHooks.beforeMount(el, binding, vnode)
    }
  },
  
  mounted(el, binding, vnode) {
    // 公共逻辑
    el.dataset.directiveMounted = 'true'
    
    // 调用自定义钩子
    if (customHooks.mounted) {
      customHooks.mounted(el, binding, vnode)
    }
  },
  
  // ... 其他钩子
})

// 创建特定指令
const vCustomDirective = baseDirective({
  mounted(el, binding) {
    // 特定逻辑
    el.style.color = binding.value
  }
})

5.2 错误处理

javascript 复制代码
// 指令错误处理
const vSafeDirective = {
  mounted(el, binding) {
    try {
      // 可能出错的操作
      const result = JSON.parse(binding.value)
      el._parsedData = result
    } catch (error) {
      // 错误处理
      console.error('指令执行错误:', error)
      
      // 显示错误状态
      el.classList.add('directive-error')
      el.title = `指令错误: ${error.message}`
      
      // 触发错误事件
      el.dispatchEvent(new CustomEvent('directive-error', {
        detail: { error, binding }
      }))
    }
  },
  
  updated(el, binding) {
    // 检查值是否有效
    if (binding.value === undefined || binding.value === null) {
      console.warn('指令接收到无效值')
      return
    }
    
    // 继续执行
    this.mounted(el, binding)
  },
  
  beforeUnmount(el) {
    // 清理
    el.classList.remove('directive-error')
    delete el._parsedData
  }
}

六、总结

6.1 生命周期钩子关键点

javascript 复制代码
// 记忆口诀
const directiveLifecycle = `
口诀一:
挂载前准备(beforeMount)
挂载后执行(mounted)
更新前检查(beforeUpdate)
更新后响应(updated)
卸载前清理(beforeUnmount)
卸载后释放(unmounted)

口诀二:
绑定看 beforeMount + mounted
更新看 beforeUpdate + updated
解绑看 beforeUnmount + unmounted

口诀三:
Vue2 转 Vue3:
bind → beforeMount
inserted → mounted
update → beforeUpdate
componentUpdated → updated
unbind → beforeUnmount + unmounted
`

// 使用建议
const bestPractices = `
1. 在 mounted 中操作 DOM
2. 在 updated 中响应数据变化
3. 在 beforeUnmount/unmounted 中清理资源
4. 使用指令参数和修饰符增强功能
5. 考虑性能,合理使用事件监听
6. 保持指令的单一职责
7. 添加错误处理
8. 提供清理函数避免内存泄漏
`

6.2 完整示例:图片懒加载指令

javascript 复制代码
const vLazyLoad = {
  beforeMount(el, binding) {
    // 初始化
    el._lazyLoadObserver = null
    el._lazyLoadSrc = binding.value
    el._lazyLoadOptions = {
      root: null,
      rootMargin: '50px',
      threshold: 0.1,
      ...(typeof binding.value === 'object' ? binding.value.options : {})
    }
  },
  
  mounted(el, binding) {
    // 设置占位符
    const placeholder = el.getAttribute('data-src') || '/placeholder.jpg'
    el.setAttribute('src', placeholder)
    
    // 获取真实图片地址
    const src = typeof binding.value === 'string' 
      ? binding.value 
      : binding.value.src
    
    // 如果图片已经在视窗内,直接加载
    if (isElementInViewport(el)) {
      loadImage(el, src)
      return
    }
    
    // 使用 IntersectionObserver 懒加载
    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          loadImage(el, src)
          observer.unobserve(el)
        }
      })
    }, el._lazyLoadOptions)
    
    observer.observe(el)
    el._lazyLoadObserver = observer
  },
  
  updated(el, binding) {
    // 如果图片地址变化
    const newSrc = typeof binding.value === 'string' 
      ? binding.value 
      : binding.value.src
    
    if (newSrc !== el._lazyLoadSrc) {
      // 停止观察
      if (el._lazyLoadObserver) {
        el._lazyLoadObserver.unobserve(el)
        el._lazyLoadObserver = null
      }
      
      // 重新设置
      el._lazyLoadSrc = newSrc
      
      // 如果已经在视窗内,直接加载
      if (isElementInViewport(el)) {
        loadImage(el, newSrc)
      } else {
        // 重新观察
        const observer = new IntersectionObserver((entries) => {
          entries.forEach(entry => {
            if (entry.isIntersecting) {
              loadImage(el, newSrc)
              observer.unobserve(el)
            }
          })
        }, el._lazyLoadOptions)
        
        observer.observe(el)
        el._lazyLoadObserver = observer
      }
    }
  },
  
  beforeUnmount(el) {
    // 清理 IntersectionObserver
    if (el._lazyLoadObserver) {
      el._lazyLoadObserver.unobserve(el)
      el._lazyLoadObserver = null
    }
  }
}

// 工具函数
function isElementInViewport(el) {
  const rect = el.getBoundingClientRect()
  return (
    rect.top <= (window.innerHeight || document.documentElement.clientHeight) &&
    rect.left <= (window.innerWidth || document.documentElement.clientWidth) &&
    rect.bottom >= 0 &&
    rect.right >= 0
  )
}

function loadImage(el, src) {
  const img = new Image()
  
  img.onload = () => {
    el.setAttribute('src', src)
    el.classList.add('loaded')
    
    // 触发加载完成事件
    el.dispatchEvent(new CustomEvent('lazyloaded', {
      detail: { src, element: el }
    }))
  }
  
  img.onerror = () => {
    console.error(`图片加载失败: ${src}`)
    el.classList.add('error')
    
    // 触发错误事件
    el.dispatchEvent(new CustomEvent('lazyloaderror', {
      detail: { src, element: el }
    }))
  }
  
  img.src = src
}

// 用法示例
// <img v-lazy-load="'/images/picture.jpg'">
// <img v-lazy-load="{ src: '/images/picture.jpg', options: { rootMargin: '100px' } }">

核心要点:

  1. Vue 3 指令有 6 个生命周期钩子:beforeMount、mounted、beforeUpdate、updated、beforeUnmount、unmounted
  2. 正确选择钩子:根据操作类型选择合适的时间点
  3. 资源管理:在卸载钩子中清理事件监听、定时器、观察器等
  4. 性能优化:合理使用防抖、节流、事件委托
  5. 错误处理:增强指令的健壮性

掌握指令生命周期钩子,你可以创建强大、可复用、高性能的自定义指令,极大地扩展 Vue 的能力边界!

相关推荐
是小崔啊2 小时前
03-vue2
前端·javascript·vue.js
学习非暴力沟通的程序员2 小时前
Karabiner-Elements 豆包语音输入一键启停操作手册
前端
Jing_Rainbow2 小时前
【 前端三剑客-39 /Lesson65(2025-12-12)】从基础几何图形到方向符号的演进与应用📐➡️🪜➡️🥧➡️⭕➡️🛞➡️🧭
前端·css·html
刘羡阳2 小时前
使用Web Worker的经历
前端·javascript
!执行2 小时前
高德地图 JS API 在 Linux 系统的兼容性解决方案
linux·前端·javascript
Gooooo2 小时前
现代浏览器的工作原理
前端
kk晏然3 小时前
TypeScript 错误类型检查,前端ts错误指南
前端·react native·typescript·react
纆兰3 小时前
汇款单的完成
前端·javascript·html
Lsx_3 小时前
案例+图解带你遨游 Canvas 2D绘图 Fabric.js🔥🔥(5W+字)
前端·javascript·canvas