Vue 自定义指令
Vue 允许你注册自定义指令来封装 DOM 操作行为。本章将详细介绍自定义指令的用法。
基本概念
什么是自定义指令?
自定义指令是 Vue 提供的扩展机制,允许你创建可复用的 DOM 操作逻辑。
┌─────────────────────────────────────────────────────────┐
│ 自定义指令生命周期 │
├─────────────────────────────────────────────────────────┤
│ │
│ created → mounted → updated → unmounted │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ 元素创建 元素挂载 元素更新 元素卸载 │
│ │
└─────────────────────────────────────────────────────────┘
创建自定义指令
全局注册
// main.js
const app = createApp(App)
// 注册 v-focus 指令
app.directive('focus', {
mounted(el) {
el.focus()
}
})
app.mount('#app')
局部注册
<script setup>
const vFocus = {
mounted(el) {
el.focus()
}
}
</script>
<template>
<input v-focus />
</template>
指令钩子函数
钩子参数
| 钩子 | 说明 |
|---|---|
created | 元素创建时调用 |
beforeMount | 元素挂载前调用 |
mounted | 元素挂载后调用 |
beforeUpdate | 元素更新前调用 |
updated | 元素更新后调用 |
beforeUnmount | 元素卸载前调用 |
unmounted | 元素卸载后调用 |
钩子参数详解
app.directive('my-directive', {
// el: 指令绑定的 DOM 元素
// binding: 包含以下属性的对象
// - value: 指令绑定的值
// - oldValue: 上一次的值
// - arg: 指令参数
// - modifiers: 修饰符对象
// - instance: 组件实例
// - dir: 指令定义对象
// vnode: 虚拟节点
// prevNode: 上一个虚拟节点
created(el, binding, vnode, prevNode) {
console.log('元素创建')
},
beforeMount(el, binding) {
console.log('挂载前')
},
mounted(el, binding) {
console.log('挂载后')
},
beforeUpdate(el, binding) {
console.log('更新前')
},
updated(el, binding) {
console.log('更新后')
},
beforeUnmount(el, binding) {
console.log('卸载前')
},
unmounted(el, binding) {
console.log('卸载后')
}
})
实际应用示例
v-focus 自动聚焦
// v-focus.js
export const vFocus = {
mounted(el) {
el.focus()
}
}
<script setup>
import { vFocus } from './directives/vFocus'
</script>
<template>
<input v-focus type="text" />
</template>
v-click-outside 点击外部
// v-click-outside.js
export const vClickOutside = {
mounted(el, binding) {
el._clickOutside = (event) => {
if (!el.contains(event.target)) {
binding.value(event)
}
}
document.addEventListener('click', el._clickOutside)
},
unmounted(el) {
document.removeEventListener('click', el._clickOutside)
}
}
<script setup>
import { vClickOutside } from './directives/vClickOutside'
function handleClickOutside() {
console.log('点击了外部')
}
</script>
<template>
<div v-click-outside="handleClickOutside">
点击外部会触发
</div>
</template>
v-longpress 长按
// v-longpress.js
export const vLongpress = {
beforeMount(el, binding) {
let pressTimer = null
const start = (e) => {
if (e.type === 'mousedown' && e.button !== 0) return
pressTimer = setTimeout(() => {
binding.value(e)
}, binding.arg || 500)
}
const cancel = () => {
if (pressTimer !== null) {
clearTimeout(pressTimer)
pressTimer = null
}
}
el._longpress = { start, cancel }
el.addEventListener('mousedown', start)
el.addEventListener('touchstart', start)
el.addEventListener('mouseup', cancel)
el.addEventListener('mouseleave', cancel)
el.addEventListener('touchend', cancel)
},
unmounted(el) {
const { start, cancel } = el._longpress
el.removeEventListener('mousedown', start)
el.removeEventListener('touchstart', start)
el.removeEventListener('mouseup', cancel)
el.removeEventListener('mouseleave', cancel)
el.removeEventListener('touchend', cancel)
}
}
<script setup>
import { vLongpress } from './directives/vLongpress'
function handleLongpress() {
alert('长按触发!')
}
</script>
<template>
<button v-longpress="1000" @click="handleClick">
长按我
</button>
</template>
v-lazy-load 图片懒加载
// v-lazy-load.js
export const vLazyLoad = {
mounted(el, binding) {
const options = {
rootMargin: binding.arg || '50px',
threshold: 0.1
}
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
el.src = binding.value
observer.unobserve(el)
}
})
}, options)
el._observer = observer
observer.observe(el)
},
unmounted(el) {
el._observer?.disconnect()
}
}
<script setup>
import { vLazyLoad } from './directives/vLazyLoad'
</script>
<template>
<img v-lazy-load="0" :src="lazySrc" alt="懒加载图片" />
</template>
v-draggable 拖拽
// v-draggable.js
export const vDraggable = {
mounted(el) {
let isDragging = false
let startX, startY, initialX, initialY
const onMouseDown = (e) => {
isDragging = true
startX = e.clientX
startY = e.clientY
const rect = el.getBoundingClientRect()
initialX = rect.left
initialY = rect.top
document.addEventListener('mousemove', onMouseMove)
document.addEventListener('mouseup', onMouseUp)
}
const onMouseMove = (e) => {
if (!isDragging) return
const dx = e.clientX - startX
const dy = e.clientY - startY
el.style.position = 'fixed'
el.style.left = `${initialX + dx}px`
el.style.top = `${initialY + dy}px`
}
const onMouseUp = () => {
isDragging = false
document.removeEventListener('mousemove', onMouseMove)
document.removeEventListener('mouseup', onMouseUp)
}
el.addEventListener('mousedown', onMouseDown)
el._cleanup = () => {
el.removeEventListener('mousedown', onMouseDown)
}
},
unmounted(el) {
el._cleanup?.()
}
}
带参数和修饰符的指令
指令参数
app.directive('color', {
mounted(el, binding) {
el.style.color = binding.value
},
updated(el, binding) {
el.style.color = binding.value
}
})
<template>
<p v-color="'red'">红色文字</p>
<p v-color="textColor">动态颜色</p>
</template>
指令修饰符
app.directive('border', {
mounted(el, binding) {
if (binding.modifiers.top) {
el.style.borderTop = '1px solid #ddd'
}
if (binding.modifiers.bottom) {
el.style.borderBottom = '1px solid #ddd'
}
if (binding.arg) {
el.style.borderWidth = binding.arg
}
}
})
<template>
<!-- 顶部边框,粗细 2px -->
<div v-border.top:2px>顶部边框</div>
<!-- 底部边框 -->
<div v-border.bottom>底部边框</div>
<!-- 上下边框 -->
<div v-border.top.bottom>上下边框</div>
</template>
简化写法
当指令只需要 mounted 和 updated 钩子时,可以简写:
// 完整写法
app.directive('color', {
mounted(el, binding) {
el.style.color = binding.value
},
updated(el, binding) {
el.style.color = binding.value
}
})
// 简化写法
app.directive('color', (el, binding) => {
el.style.color = binding.value
})
小结
本章我们详细学习了 Vue 自定义指令的完整内容:
- 自定义指令概念:封装 DOM 操作行为
- 指令生命周期:created 到 unmounted
- 钩子参数详解:el、binding、vnode 等
- 实际应用:focus、click-outside、longpress、lazy-load、draggable
- 参数和修饰符:传递数据和配置指令行为
- 简化写法:简化的函数写法
练习
- 创建一个
v-permission指令控制元素显示 - 创建一个
v-resize指令监听元素大小变化 - 创建一个
v-tooltip指令实现悬浮提示
准备好进入下一章了吗?