前言
该文主要是介绍一些常用的vue自定义指令,可以让我们的代码看起来更加简洁。
vue2和vue3的指令写法略有不同,主要区别就是钩子上的调整,如果已经了解过的同学,可以直接跳到demo去看看
简介
除了 Vue 内置的一系列指令 (比如 v-model 或 v-show) 之外,Vue 还允许你注册自定义的指令 (Custom Directives) 一个自定义指令由一个包含类似组件生命周期钩子的对象来定义。钩子函数会接收到指令所绑定元素作为其参数。
Vue.js 2和Vue.js 3在自定义指令方面有一些区别。
在Vue.js 2中,自定义指令使用directive选项来定义,并且需要在适当的生命周期钩子函数中编写指令的逻辑。指令的定义包括bind、update、unbind三个钩子函数,分别用于绑定指令时、更新指令时和解绑指令时执行相应的逻辑。
而在Vue.js 3引入了Composition API,使得指令的逻辑可以更容易地组织和重用。你可以使用onBeforeMount、onMounted、onBeforeUpdate、onUpdated、onBeforeUnmount等Composition API钩子函数来编写指令的逻辑。此外,Vue.js 3还引入了新的钩子函数beforeMount、mounted、beforeUpdate、updated、unmounted,用于替代Vue.js 2中的bind、update、unbind。
vue2、vue3的生命周期对照
| 钩子函数 | Vue.js 2 | Vue.js 3 |
|---|---|---|
| bind | 绑定时执行的逻辑 | 移除,使用beforeMount替代 |
| inserted | 元素插入到父节点时执行的逻辑 | 移除,可以使用mounted来代替 |
| update | 元素所在组件的VNode更新时执行的逻辑 | 使用beforeUpdate和updated替代 |
| componentUpdated | 元素所在组件的VNode及其子VNode更新时执行的逻辑 | 使用beforeUpdate和updated替代 |
| unbind | 解绑时执行的逻辑 | 使用unmounted替代 |
vue3 钩子简介
javascript
const myDirective = {
// 在绑定元素的 attribute 前
// 或事件监听器应用前调用
created(el, binding, vnode, prevVnode) {
// 下面会介绍各个参数的细节
},
// 在元素被插入到 DOM 前调用
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) {}
}
常用例子
waterMarker指令(水印)

js
function addWaterMarker (str, parentNode, font, textColor) {
// 水印文字,父元素,字体,文字颜色
var can = document.createElement('canvas')
parentNode.appendChild(can)
can.width = 200
can.height = 150
can.style.display = 'none'
var cans = can.getContext('2d')
cans.rotate((-20 * Math.PI) / 180)
cans.font = font || '16px Microsoft JhengHei'
cans.fillStyle = textColor || 'rgba(180, 180, 180, 0.3)'
cans.textAlign = 'left'
cans.textBaseline = 'Middle'
cans.fillText(str, can.width / 10, can.height / 2)
parentNode.style.backgroundImage = 'url(' + can.toDataURL('image/png') + ')'
}
const waterMarker = {
bind: function (el, binding) {
addWaterMarker(binding.value.text, el, binding.value.font, binding.value.textColor)
},
}
export default waterMarker
html
<script>
import waterMarker from '@/directives/waterMarker'
export default {
directives: {
waterMarker
}
}
</script>
<template>
<div class="text-box" v-waterMarker="{text:'超神熊猫',textColor:'rgba(0, 0, 0, 0.4)'}"></div>
</template>
copy指令(复制)

js
const copy = {
bind (el, { value }) {
el.$value = value
el.handler = () => {
if (!el.$value) {
console.log('复制内容为空')
return
}
const textarea = document.createElement('textarea')
textarea.readOnly = 'readonly'
textarea.style.position = 'absolute'
textarea.style.left = '-9999px'
textarea.value = el.$value
document.body.appendChild(textarea)
textarea.select()
if (navigator.clipboard) {
navigator.clipboard.writeText(el.$value).then(() => {
console.log('复制成功', el.$value)
})
} else {
// execCommand即将被废弃
const result = document.execCommand('Copy')
if (result) {
console.log('复制成功', el.$value)
}
}
document.body.removeChild(textarea)
}
el.addEventListener('click', el.handler)
},
componentUpdated (el, { value }) {
el.$value = value
},
unbind (el) {
el.removeEventListener('click', el.handler)
},
}
export default copy
clickOutside指令(点击元素外部)

js
const clickOutside = {
// 初始化指令
bind(el, binding) {
function clickHandler(e) {
if (el.contains(e.target)) {
return false;
}
if (binding.expression) {
binding.value(e);
}
}
el.__vueClickOutside__ = clickHandler;
document.addEventListener("click", clickHandler);
},
unbind(el) {
document.removeEventListener("click", el.__vueClickOutside__);
delete el.__vueClickOutside__;
},
}
export default clickOutside
longPress指令(长按)

js
const longPress = {
bind: function (el, binding, vNode) {
if (typeof binding.value !== 'function') {
throw 'callback must be a function'
}
// 定义变量
let pressTimer = null
// 创建计时器( 2秒后执行函数 )
let start = (e) => {
if (e.type === 'click' && e.button !== 0) {
return
}
if (pressTimer === null) {
pressTimer = setTimeout(() => {
handler()
}, 2000)
}
}
// 取消计时器
let cancel = (e) => {
if (pressTimer !== null) {
clearTimeout(pressTimer)
pressTimer = null
}
}
// 运行函数
const handler = (e) => {
binding.value(e)
}
// 添加事件监听器
el.addEventListener('mousedown', start)
el.addEventListener('touchstart', start)
// 取消计时器
el.addEventListener('click', cancel)
el.addEventListener('mouseout', cancel)
el.addEventListener('touchend', cancel)
el.addEventListener('touchcancel', cancel)
},
// 当传进来的值更新的时候触发
componentUpdated (el, { value }) {
el.$value = value
},
// 指令与元素解绑的时候,移除事件绑定
unbind (el) {
el.removeEventListener('click', el.handler)
},
}
export default longPress
debounce指令(防抖)

js
const debounce = {
inserted: function (el, { value }) {
// 可支持仅传入函数,也可支持传入函数和延迟时间配置
let callback = null
let time = 1000
if (typeof value == 'function') {
callback = value
} else if (value.callback && typeof value.callback == 'function') {
callback = value.callback
time = value.time || 1000
}
if (!callback) {
throw 'Must be passed into the callback function'
}
let timer = null
el.addEventListener('click', () => {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
callback()
}, time)
})
},
}
export default debounce
html
<script>
import debounce from '@/directives/debounce'
export default {
directives: {
debounce
},
methods: {
debounceClick () {
console.log('只触发一次')
}
}
}
</script>
<template>
<div>
<button v-debounce="debounceClick">防抖(默认1秒)</button>
<button v-debounce="{ callback: debounceClick, time: 2000 }">防抖(传入2秒)</button>
</div>
</template>
rightClick指令(右键点击)

js
const rightClick = {
bind: function(el, binding, vnode) {
// 绑定事件处理程序
el.addEventListener('contextmenu', function(event) {
event.preventDefault() // 阻止默认的右键菜单显示
binding.value(event) // 调用指令绑定的回调函数,并传入鼠标事件对象
})
}
}
export default rightClick
html
<script>
import rightClick from '@/directives/rightClick'
export default {
directives: {
rightClick
},
methods: {
rightClickFn(event) {
console.log('点击右键', event)
}
}
}
</script>
<template>
<div>
<div class="dom-box" v-rightClick="rightClickFn">
内容
</div>
</div>
</template>
右键点击菜单

js
const rightClickMenu = {
bind: function(el, binding, vnode) {
el.addEventListener('contextmenu', function(event) {
event.preventDefault()
if (vnode.context.menuVisible) {
return false // 如果菜单已经显示,则不再重复弹出
}
vnode.context.menuVisible = true // 设置菜单为显示状态
const { menuItems = [] } = binding.value
let menu = document.createElement('ul')
menu.style.position = 'fixed'
menu.style.zIndex = 999
menu.style.top = event.clientY + 'px'
menu.style.left = event.clientX + 'px'
menuItems.forEach((item) => {
const menuItem = document.createElement('li')
menuItem.innerText = item.text
menuItem.addEventListener('click', function() {
vnode.context.menuVisible = false // 点击菜单项后隐藏菜单
item.handler()
})
menu.appendChild(menuItem)
})
document.body.appendChild(menu)
function deleteMenu(e) {
if (!el.contains(e.target)) {
vnode.context.menuVisible = false
if (menu) document.body.removeChild(menu)
menu = null
}
}
// 点击其他地方时隐藏菜单
el.__vueDeleteMenu__ = deleteMenu
document.addEventListener("click", deleteMenu)
})
},
unbind(el) {
document.removeEventListener("click", el.__vueDeleteMenu__)
delete el.__vueDeleteMenu__
},
}
export default rightClickMenu
html
<script>
import rightClickMenu from '@/directives/rightClickMenu'
export default {
directives: {
rightClickMenu
},
methods: {
getRightClickMenu() {
return {
menuItems: [
{ id: 1, text: '菜单项1', handler: () => console.log('点击了菜单项1') },
{ id: 2, text: '菜单项2', handler: () => console.log('点击了菜单项2') },
{ id: 3, text: '菜单项3', handler: () => console.log('点击了菜单项3') }
]
}
}
}
}
</script>
<template>
<div>
<div class="dom-box" v-rightClickMenu="getRightClickMenu()">
内容
</div>
</div>
</template>
文本高亮指令

js
const highlight = {
bind: function (el, binding) {
setHighlight(el, binding)
},
update: function (el, binding) {
setHighlight(el, binding)
}
}
function setHighlight(el, binding) {
const { value } = binding
let _keywords = value
let _color = 'red'
if (typeof value === 'object') {
const { keywords = '', color = _color } = value
_keywords = keywords
_color = color
}
if (_keywords) {
_keywords = _keywords.split('|')
const regex = new RegExp(`(${_keywords.join('|')})`, 'gi')
const highlightedText = el.innerText.replace(regex, `<span style="color: ${_color};">$1</span>`)
el.innerHTML = highlightedText
} else {
el.innerHTML = el.innerText
}
}
export default highlight
html
<script>
import highlight from '@/directives/highlight'
export default {
directives: {
highlight
},
data() {
return {
value: ''
}
}
}
</script>
<template>
<div>
<input type="text" v-model="value">
<p v-highlight="value">内容:123456789</p>
</div>
</template>
输入框禁止输入表情符号

js
let findEle = (parent, type) => {
return parent.tagName.toLowerCase() === type ? parent : parent.querySelector(type)
}
const trigger = (el, type) => {
const e = document.createEvent('HTMLEvents')
e.initEvent(type, true, true)
el.dispatchEvent(e)
}
const emoji = {
bind: function (el, binding, vnode) {
// 正则规则可根据需求自定义
var regRule = /[^\u4E00-\u9FA5|\d|\a-zA-Z|\r\n\s,.?!,。?!...---&$=()-+/*{}[\]]|\s/g
let $inp = findEle(el, 'input')
el.$inp = $inp
$inp.handle = function () {
let val = $inp.value
$inp.value = val.replace(regRule, '')
trigger($inp, 'input')
}
$inp.addEventListener('keyup', $inp.handle)
},
unbind: function (el) {
el.$inp.removeEventListener('keyup', el.$inp.handle)
},
}
export default emoji
html
<script>
import emoji from '@/directives/emoji'
export default {
directives: {
emoji
},
data() {
return {
value: ''
}
},
}
</script>
<template>
<input type="text" v-emoji v-model="value">
</template>
总结
vue指令可以将很多常用的功能简化封装起来,是否真的有必要封装指令也是值得思考的一件事。有些样式指令(如:增加某个指令可以改变样式之类的),可能还不如直接规范的写好公共样式,指令虽好,但切勿滥用