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' } }">
核心要点:
- Vue 3 指令有 6 个生命周期钩子:beforeMount、mounted、beforeUpdate、updated、beforeUnmount、unmounted
- 正确选择钩子:根据操作类型选择合适的时间点
- 资源管理:在卸载钩子中清理事件监听、定时器、观察器等
- 性能优化:合理使用防抖、节流、事件委托
- 错误处理:增强指令的健壮性
掌握指令生命周期钩子,你可以创建强大、可复用、高性能的自定义指令,极大地扩展 Vue 的能力边界!