一. Vue 3 中自定义指令的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 自定义指令名称为v-directive-name
app.directive('directive-name', {
// 指令绑定到元素时触发,el是指令所绑定的元素,binding是一个对象,包含指令的信息
mounted(el, binding) {
// 指令逻辑
},
// 指令与元素解绑时触发
unmounted(el) {
// 指令逻辑
},
// 指令所在元素被更新时触发
updated(el, binding) {
// 指令逻辑
},
});

二. 常用的自定义指令:

v-focus:自动聚焦到输入框中

1
2
3
4
5
app.directive('focus', {
mounted(el) {
el.focus()
}
})

v-copy:复制指令绑定的内容到剪贴板,当元素被点击时,执行复制操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
app.directive('copy', {
mounted(el, binding) {
el.addEventListener('click', () => {
const textarea = document.createElement('textarea')
textarea.value = binding.value
textarea.style.position = 'absolute'
textarea.style.top = '-9999px'
document.body.appendChild(textarea)
textarea.select()
document.execCommand('copy')
textarea.remove()
})
}
})

使用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<button v-copy="message">Copy message</button>
</template>

<script>
export default {
data() {
return {
message: 'Hello, world!'
}
}
}
</script>

v-click-outside:在元素外部点击时触发绑定的事件,并移除绑定的 document 点击事件监听器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
app.directive('click-outside', {
mounted(el, binding) {
const handleClickOutside = (e) => {
if (!el.contains(e.target) && el !== e.target) {
binding.value()
}
}
document.addEventListener('click', handleClickOutside)
el.handleClickOutside = handleClickOutside
},
unmounted(el) {
document.removeEventListener('click', el.handleClickOutside)
}
})

使用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div v-click-outside="closeModal">
<div class="modal">Modal content</div>
</div>
</template>

<script>
export default {
methods: {
closeModal() {
// 关闭模态框
}
}
}
</script>

v-throttle:限制事件触发的频率,移除绑定的事件监听器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
app.directive('throttle', {
mounted(el, binding) {
const wait = parseInt(binding.arg) || 300
const func = binding.value

el.throttleHandler = throttle(func, wait)

el.addEventListener('click', el.throttleHandler)

function throttle(fn, wait) {
let lastTime = 0
return function() {
const now = new Date().getTime()
if (now - lastTime > wait) {
fn.apply(this, arguments)
lastTime = now
}
}
}
},
unmounted(el) {
el.removeEventListener('click', el.throttleHandler)
}
})

在这里,throttle 是一个工具函数,用于创建一个具有节流效果的函数。它的实现方式是,保存上一次执行该函数的时间戳,每次执行时比较当前时间戳和上一次时间戳之间的时间差,如果时间差大于设定的阈值,则执行该函数,否则不执行。
el.throttleHandler 是一个函数,它是 throttle 函数的返回值,也就是一个具有节流效果的函数。在这个指令中,我们把它绑定到了元素上,以便在元素被卸载时可以将其移除。
在指令的 mounted 钩子中,我们从绑定中获取节流阈值 wait 和需要节流的函数 func,然后使用 throttle 工具函数创建一个具有节流效果的函数,并将其添加到元素的 click 事件监听器中。在元素被卸载时,我们移除该事件监听器并将 el.throttleHandler 函数从元素上删除。
使用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<input v-model="message" v-throttle:500ms="handleChange">
</template>

<script>
export default {
data() {
return {
message: ''
}
},
methods: {
handleChange() {
// 处理函数
}
}
}
</script>

v-debounce:限制事件触发的频率,移除绑定的事件监听器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
app.directive('debounce', {
mounted(el, binding) {
const wait = parseInt(binding.arg) || 300
const func = binding.value

el.debounceHandler = debounce(func, wait)

el.addEventListener('input', el.debounceHandler)

function debounce(fn, wait) {
let timer = null
return function() {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
fn.apply(this, arguments)
}, wait)
}
}
},
unmounted(el) {
el.removeEventListener('input', el.debounceHandler)
}
})

在这里,debounce 是一个工具函数,用于创建一个具有防抖效果的函数。它的实现方式是,使用定时器在等待一段时间之后执行该函数,如果在等待时间内再次触发该函数,则清除上一次的定时器并重新开始等待。
el.debounceHandler 是一个函数,它是 debounce 函数的返回值,也就是一个具有防抖效果的函数。在这个指令中,我们把它绑定到了元素上,以便在元素被卸载时可以将其移除。
在指令的 mounted 钩子中,我们从绑定中获取防抖等待时间 wait 和需要防抖的函数 func,然后使用 debounce 函数创建具有防抖效果的函数 el.debounceHandler,并将其绑定到元素上。同时,我们还在元素上添加了一个 input 事件监听器,将防抖处理器作为回调函数,从而实现了防抖效果。
在指令的 unmounted 钩子中,我们移除了该元素上的 input 事件监听器,以防止在元素卸载时出现内存泄漏的情况。
使用 v-debounce 指令时,可以在绑定中通过参数传递防抖等待时间,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<input v-model="message" v-debounce:500ms="handleChange">
</template>

<script>
export default {
data() {
return {
message: ''
}
},
methods: {
handleChange() {
// 处理函数
}
}
}
</script>

这里的 v-debounce:500ms 表示使用 500 毫秒的等待时间来实现防抖效果,handleChange 是处理函数。

v-mask:在元素上创建遮罩层,移除创建的遮罩层。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
app.directive('mask', {
mounted(el, binding) {
const mask = document.createElement('div')
// ...
el.mask = mask
el.showMask = () => {
el.mask.style.display = 'block'
}
el.hideMask = () => {
el.mask.style.display = 'none'
}

if (binding.value) {
el.showMask()
} else {
el.hideMask()
}
},
updated(el, binding) {
// ...
},
unmounted(el) {
el.mask.remove()
}
})

v-ellipsis:超出指定行数时省略文本并添加提示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
app.directive('ellipsis', {
mounted(el, binding) {
const style = window.getComputedStyle(el, null)
const lineHeight = parseInt(style.lineHeight)
const maxHeight = parseInt(style.maxHeight)
const text = el.innerText
const ellipsis = '...'

if (el.offsetHeight < maxHeight) {
return
}

const binarySearch = (min, max) => {
const mid = Math.floor((min + max) / 2)
const subText = text.slice(0, mid) + ellipsis
el.innerText = subText

if (min > max - 2) {
return
}

if (el.offsetHeight <= maxHeight) {
binarySearch(mid, max)
} else {
binarySearch(min, mid)
}
}

binarySearch(0, text.length - 1)
},
unmounted(el) {
el.innerText = el.__v_ellipsis_text
},
beforeUpdate(el) {
el.__v_ellipsis_text = el.innerText
},
updated(el) {
if (el.innerText !== el.__v_ellipsis_text) {
app.directive['ellipsis'].unmounted(el)
app.directive['ellipsis'].mounted(el)
}
}
})

使用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div v-ellipsis>{{ longText }}</div>
</template>

<script>
export default {
data() {
return {
longText: 'This is a long text that needs to be truncated with ellipsis.'
}
}
}
</script>

v-drag:拖拽,当鼠标按下并移动时,移动元素的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
app.directive('drag', {
mounted(el) {
let startX, startY, initialX, initialY, xOffset = 0, yOffset = 0
el.addEventListener('mousedown', dragStart)
el.addEventListener('touchstart', dragStart, { passive: true })
el.addEventListener('mouseup', dragEnd)
el.addEventListener('touchend', dragEnd)
el.addEventListener('mousemove', drag)
el.addEventListener('touchmove', drag, { passive: true })

function dragStart(e) {
if (e.type === 'touchstart') {
initialX = e.touches[0].clientX - xOffset
initialY = e.touches[0].clientY - yOffset
} else {
initialX = e.clientX - xOffset
initialY = e.clientY - yOffset
}

if (e.target === el) {
startX = initialX
startY = initialY
}
}

function dragEnd(e) {
initialX = e.clientX - xOffset
initialY = e.clientY - yOffset

startX = initialX
startY = initialY

el.style.transform = `translate3d(${xOffset}px, ${yOffset}px, 0)`
}

function drag(e) {
if (e.type === 'touchmove') {
xOffset = e.touches[0].clientX - initialX
yOffset = e.touches[0].clientY - initialY
} else {
xOffset = e.clientX - initialX
yOffset = e.clientY - initialY
}

el.style.transform = `translate3d(${xOffset}px, ${yOffset}px, 0)`
}
}
})

v-draggable:允许元素拖动,释放时移除绑定的事件监听器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
app.directive('draggable', {
mounted(el) {
el.style.position = 'absolute'
let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0

el.onmousedown = dragMouseDown

function dragMouseDown(e) {
e.preventDefault()
pos3 = e.clientX
pos4 = e.clientY
document.onmouseup = closeDragElement
document.onmousemove = elementDrag
}

function elementDrag(e) {
e.preventDefault()
pos1 = pos3 - e.clientX
pos2 = pos4 - e.clientY
pos3 = e.clientX
pos4 = e.clientY
el.style.top = (el.offsetTop - pos2) + 'px'
el.style.left = (el.offsetLeft - pos1) + 'px'
}

function closeDragElement() {
document.onmouseup = null
document.onmousemove = null
}
},
unmounted(el) {
el.onmousedown = null
}
})

v-clipboard:在元素上绑定复制事件,复制成功时移除绑定的事件监听器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
app.directive('clipboard', {
mounted(el, binding) {
const clipboardHandler = () => {
const input = document.createElement('input')
input.setAttribute('value', binding.value)
document.body.appendChild(input)
input.select()
document.execCommand('copy')
document.body.removeChild(input)
binding.arg()
}
el.addEventListener('click', clipboardHandler)
el.clipboardHandler = clipboardHandler
},
unmounted(el) {
el.removeEventListener('click', el.clipboardHandler)
}
})

v-ripple:水波纹效果,当鼠标点击元素时,会在元素内部产生水波纹效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
app.directive('ripple', {
mounted(el) {
el.addEventListener('mousedown', e => {
const ripple = document.createElement('span')
const size = Math.max(el.clientWidth, el.clientHeight)
const pos = el.getBoundingClientRect()
const x = e.clientX - pos.left - size / 2
const y = e.clientY - pos.top - size / 2
ripple.style.width = ripple.style.height = size + 'px'
ripple.style.left = x + 'px'
ripple.style.top = y + 'px'
ripple.classList.add('ripple')
el.appendChild(ripple)
setTimeout(() => {
ripple.remove()
}, 1000)
})
}
})

使用方法:

1
2
3
4
<template>
<button v-ripple>Button with ripple effect</button>
</template>

v-resize:在元素大小改变时触发事件

1
2
3
4
5
6
7
8
9
10
11
12
13
app.directive('resize', {
mounted(el, binding) {
const onResize = e => {
binding.value(e)
}
window.addEventListener('resize', onResize)
el._onResize = onResize
},
unmounted(el) {
window.removeEventListener('resize', el._onResize)
}
})

v-long-press:长按触发事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
app.directive('long-press', {
mounted(el, binding) {
let timeout
const start = e => {
if (e.type === 'click' && e.button !== 0) {
return
}
if (timeout) clearTimeout(timeout)
timeout = setTimeout(() => {
binding.value(e)
}, 1000)
}
const cancel = () => {
if (timeout) clearTimeout(timeout)
}
el.addEventListener('mousedown', start)
el.addEventListener('touchstart', start, { passive: true })
el.addEventListener('click', cancel)
el.addEventListener('mouseout', cancel)
el.addEventListener('touchend', cancel)
el.addEventListener('touchcancel', cancel)
el._start = start
el._cancel = cancel
},
unmounted(el) {
el.removeEventListener('mousedown', el._start)
el.removeEventListener('touchstart', el._start)
el.removeEventListener('click', el._cancel)
el.removeEventListener('mouseout', el._cancel)
el.removeEventListener('touchend', el._cancel)
el.removeEventListener('touchcancel', el._cancel)
}
})

v-img-lazyload:懒加载图片,在加载完成或出错时移除绑定的 IntersectionObserver 实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
app.directive('img-lazyload', {
mounted(el, binding) {
const observer = new IntersectionObserver((entries) => {
const entry = entries[0]
if (entry.isIntersecting) {
el.src = binding.value
el.addEventListener('load', handleLoad)
el.addEventListener('error', handleError)
observer.disconnect()
}
})
observer.observe(el)

function handleLoad() {
el.removeAttribute('data-src')
el.removeEventListener('load', handleLoad)
el.removeEventListener('error', handleError)
}

function handleError() {
el.removeAttribute('data-src')
el.removeEventListener('load', handleLoad)
el.removeEventListener('error', handleError)
}
},
unmounted(el) {
el.removeEventListener('load', el.handleLoad)
el.removeEventListener('error', el.handleError)
el.observer.disconnect()
}
})

v-tooltip:在元素上绑定鼠标事件,显示提示框并在隐藏后移除绑定的事件监听器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
app.directive('tooltip', {
mounted(el, binding) {
let tooltip = null

el.addEventListener('mouseenter', handleMouseEnter)
el.addEventListener('mouseleave', handleMouseLeave)

function handleMouseEnter(e) {
tooltip = document.createElement('div')
tooltip.className = 'tooltip'
tooltip.innerHTML = binding.value
document.body.appendChild(tooltip)

positionTooltip()
}

function handleMouseLeave(e) {
if (tooltip) {
tooltip.remove()
tooltip = null
}
}

function positionTooltip() {
const rect = el.getBoundingClientRect()

const tooltipRect = tooltip.getBoundingClientRect()
const tooltipWidth = tooltipRect.width
const tooltipHeight = tooltipRect.height

const tooltipLeft = rect.left + (rect.width - tooltipWidth) / 2
const tooltipTop = rect.top - tooltipHeight - 10

tooltip.style.left = tooltipLeft + 'px'
tooltip.style.top = tooltipTop + 'px'
}
},
unmounted(el) {
el.removeEventListener('mouseenter', el.handleMouseEnter)
el.removeEventListener('mouseleave', el.handleMouseLeave)
}
})

这个指令在元素上添加鼠标移入/移出事件监听器。当鼠标移入元素时,它会在 document.body 中创建一个 div 元素作为提示框,并将绑定值的内容设置为其 innerHTML。然后它会调用 positionTooltip 函数来计算并设置提示框的位置。
在鼠标移出元素时,提示框会被删除。这个指令的实现比较简单,但可以很方便地为元素添加提示框功能。
使用方法:

1
2
3
4
<template>
<div v-tooltip="'This is a tooltip.'">Hover me</div>
</template>

v-scroll:绑定滚动事件,当元素滚动时调用绑定的回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
app.directive('scroll', {
mounted(el, binding) {
el.addEventListener('scroll', binding.value)
el.scrollHandler = binding.value
},
unmounted(el) {
el.removeEventListener('scroll', el.scrollHandler)
},
updated(el, binding) {
el.removeEventListener('scroll', el.scrollHandler)
el.addEventListener('scroll', binding.value)
el.scrollHandler = binding.value
}
})

使用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template>
<div v-scroll="onScroll">Scrollable area</div>
</template>

<script>
export default {
methods: {
onScroll(event) {
// 处理滚动事件
}
}
}
</script>

v-popover:绑定鼠标事件,显示弹出框并在隐藏后移除绑定的事件监听器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
app.directive('popover', {
mounted(el, binding) {
const popover = document.createElement('div')
popover.className = 'popover'
popover.innerHTML = binding.value
el.popover = popover

el.addEventListener('mouseenter', handleMouseEnter)
el.addEventListener('mouseleave', handleMouseLeave)

function handleMouseEnter() {
el.appendChild(popover)
}

function handleMouseLeave() {
popover.remove()
}
},
unmounted(el) {
el.removeEventListener('mouseenter', el.handleMouseEnter)
el.removeEventListener('mouseleave', el.handleMouseLeave)
el.popover.remove()
}
})

v-tap:绑定 touch 事件,当元素触发 tap 事件时调用绑定的回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
app.directive('tap', {
mounted(el, binding) {
let isTap = true
let startX, startY

el.addEventListener('touchstart', handleTouchStart)
el.addEventListener('touchmove', handleTouchMove)
el.addEventListener('touchend', handleTouchEnd)

function handleTouchStart(e) {
isTap = true
startX = e.touches[0].clientX
startY = e.touches[0].clientY
}

function handleTouchMove(e) {
const deltaX = Math.abs(e.touches[0].clientX - startX)
const deltaY = Math.abs(e.touches[0].clientY - startY)
if (deltaX > 10 || deltaY > 10) {
isTap = false
}
}

function handleTouchEnd(e) {
if (isTap) {
binding.value(e)
}
}
},
unmounted(el) {
el.removeEventListener('touchstart', el.handleTouchStart)
el.removeEventListener('touchmove', el.handleTouchMove)
el.removeEventListener('touchend', el.handleTouchEnd)
}
})

v-observe-visibility:监听元素可见性变化,移除绑定的 IntersectionObserver 实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
app.directive('observe-visibility', {
mounted(el, binding) {
const observer = new IntersectionObserver((entries) => {
const entry = entries[0]
if (entry.isIntersecting) {
binding.value(true)
} else {
binding.value(false)
}
})
el.observer = observer
observer.observe(el)
},
unmounted(el) {
el.observer.disconnect()
}
})