一. Vue 3 中自定义指令的写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| app.directive('directive-name', { 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() } })
|
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>
|
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() } })
|