文章目录
- vue3自定义指令
- 1.什么是自定义指令?
- 2.注册自定义指令
- 2.1 全局注册
- 2.2 局部注册
- `<script setup>`中注册:
- `<script>`中使用:
- 3.钩子函数参数详解
- 4.指令传值
- 5.总结
- 常用自定义指令案例
- v-longpress 长按
- v-debounce 防抖
- v-throttle 节流
- v-drag 拖拽
- `<script>`写法,对比下面`<script setup>`写法
- `<script setup>`写法,对比上面`<script >`写法
vue3自定义指令
除了 Vue 内置的一系列指令 (比如 v-model 或 v-show) 之外,Vue 还允许你注册自定义的指令 (Custom Directives)。
我们已经介绍了两种在 Vue 中重用代码的方式:组件和组合式函数。组件是主要的构建模块,而组合式函数则侧重于有状态的逻辑。另一方面,自定义指令主要是为了重用涉及普通元素的底层 DOM 访问的逻辑。
1.什么是自定义指令?
-
内置指令:
在Vue中,诸如v-if、v-for、v-on等等被称之为内置指令,它们都是以v-开头的,我们无需注册即可在全局使用它们,内置指令提供了极大的方便给我们,比如v-for指令可以让我们快速循环出很多dom元素等等 -
自定义指令:
虽然Vue已经提供了很多内置指令供我们使用,但是人都是贪婪的,总是不满足于现状。所以官方允许我们自定义指令,自定义指令就比较灵活了,我们可以使用任何名称来命名自定义指令,不过我们自定义指定还是需要以v-开头,比如v-focus、v-resize等等。
如:项目中防抖、节流、点击复制、长按识别、dom拖拽、input 元素自动聚焦这些都可以用自定义指令去完成,但让也可以封装方法,但我感觉还是写成自定义指令比较方便
最后有封装好的例子代码,不同写法,可对比学习
2.注册自定义指令
在Vue中,如果我们定义了一个组件,我们需要注册它才可以使用。自定义指令也是类似的原理,我们需要先注册自定义指令,然后才可以使用它。
2.1 全局注册
在main.ts文件中
import { createApp } from "vue";
import App from "./App.vue";
const app = createApp(App);
app.directive("focus", {
// 在绑定元素的 attribute 前
// 或事件监听器应用前调用
created(el, binding, vnode, prevVnode) {
},
// 在元素被插入到 DOM 前调用
beforeMount() {},
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted() {},
// 绑定元素的父组件更新前调用
beforeUpdate() {},
// 在绑定元素的父组件
// 及他自己的所有子节点都更新后调用
updated() {},
// 绑定元素的父组件卸载前调用
beforeUnmount() {},
// 绑定元素的父组件卸载后调用
unmounted() {},
});
app.mount("#app");
上段代码中我们借助Vue3提供的directive方法注册了一个全局的自定义指令,该方法接收两个参数:指令名称、指令钩子函数对象。
钩子函数对象和组件的生命周期一样,这也和Vue2中的自定义指令有着较大的区别。理解这些钩子函数也很简单:我们都知道自定义指令是作用在DOM元素上,那么自定义指令从绑定到DOM元素,再到DOM元素发生变化等等一系列操作,都对应了不同的钩子函数,比如当DOM元素插入到文档中时,自定义指令的mounted等钩子函数就会执行。
调用全局注册的自定义指令,代码如下:
<input type="text" v-focus>
我们可以在任意组件中调用它。
2.2 局部注册
由于Vue3中有
<script setup>
和<script>
两种写法,两种写法对应的自定义指令的注册写法不太一样。
<script setup>
中注册:
script setup写法代码更加简介,我一直推荐vue3用 secipt setup,想明白下面的写法,建议参考:vue3语法糖详解(setup()、<script setup>)
<script setup lang="ts">
// 在模板中启用 v-focus
const vFocus = {
// 在绑定元素的 attribute 前
// 或事件监听器应用前调用
created(el, binding, vnode, prevVnode) {},
// 在元素被插入到 DOM 前调用
beforeMount() {},
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted() {},
// 绑定元素的父组件更新前调用
beforeUpdate() {},
// 在绑定元素的父组件
// 及他自己的所有子节点都更新后调用
updated() {},
// 绑定元素的父组件卸载前调用
beforeUnmount() {},
// 绑定元素的父组件卸载后调用
unmounted() {},
};
</script>
在Vue3中,只要以小写字母v开头的驼峰命名的变量都可以作为一个自定义指令使用,比如上段代码中vFocus就可以在模板中通过v-focus的指令形式使用。
<script>
中使用:
export default {
setup() {
/*...*/
},
directives: {
// 在模板中启用 v-focus
focus: {
// 在绑定元素的 attribute 前
// 或事件监听器应用前调用
created(el, binding, vnode, prevVnode) {},
// 在元素被插入到 DOM 前调用
beforeMount() {},
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted() {},
// 绑定元素的父组件更新前调用
beforeUpdate() {},
// 在绑定元素的父组件
// 及他自己的所有子节点都更新后调用
updated() {},
// 绑定元素的父组件卸载前调用
beforeUnmount() {},
// 绑定元素的父组件卸载后调用
unmounted() {},
}
}
}
3.钩子函数参数详解
参考官网文档:自定义指令钩子参数
4.指令传值
我们讲解钩子函数参数时,里面有一个binding参数,这个参数是一个对象,它里面有很多属性,而这些属性中有些就是指令传的值。binding对象中的value就是。看下面代码
<div class="drag" v-drag:jie="'zhang'"></div>
<script lang="ts">
directives: {
drag: {
created(el, binding, vnode, prevVnode) {
console.log(el, binding);
},
},
},
</script>
结果看图
5.总结
自定义指令的用处非常多,如果你领略到了它的魅力,那么我相信你一定会爱上它的。Vue3和Vue2自定义指令在注册和使用上有一点,不同,不过原理都是一样的,所以如果你有Vue2的基础,学会Vue3的自定义指令简直就是信手拈来。
常用自定义指令案例
v-longpress 长按
需求:当用户按下鼠标左键或移动端单指触碰,并按住按钮几秒钟时,视为一次长按,触发对应的函数。
思路:
定义一个计时器, n 秒后执行函数,n作为参数。
当用户按下按钮时触发 mousedown 或touchstart 事件,启动计时器。
如果 click 、 mouseup 、touchend 或 touchcancel 事件在 n 秒内被触发,则清除计时器,视为普通点击事件。
如果计时器没有在 n秒内清除,则视为一次长按,触发对应的函数。
<template>
<button v-longPress="changeMsg">修改message</button>
</template>
<script setup lang="ts">
import { DirectiveBinding, ref, VNode } from 'vue';
const changeMsg = () => {
console.log('长按');
};
let vLongPress = {
created(el: HTMLElement, binding: DirectiveBinding, vnode, prevVnode) {
console.log(binding);
if (typeof binding.value !== 'function') {
const compName = vNode.context.name;
let warn = [
`longpress:`,
]`provided expression '${binding.expression}' is not afunction, but has to be `;
if (compName) {
warn += `Found in component '${compName}'`;
}
console.warn(warn);
}
// 定义变量
let pressTimer = null;
// 定义函数处理程序
// 创建计时器( 1秒后执行函数 )
let start = (e) => {
if (e.type === 'click' && e.button !== 0) {
return;
}
if (pressTimer === null) {
pressTimer = setTimeout(() => {
// 执行函数
handler();
}, 2000);
}
};
// 取消计时器
let cancel = () => {
// 检查计时器是否有值
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);
},
};
</script>
v-debounce 防抖
背景:在开发中,有时遇到要给input或者滚动条添加监听事件,需要做防抖处理。
需求:防止input或scroll事件在短时间内被多次触发,使用防抖函数限制一定时间后触发。
思路:
定义一个延迟执行的方法,如果在延迟时间内再调用该方法,则重新计算执行时间。
将事件绑定在传入的方法上。
<template>
<div>{{ message }}</div>
<!-- <button v-debounce="changeMsg">修改message</button> -->
<button v-debounce="{ fn: changeMsg, time: 3000 }">修改message</button>
</template>
<script setup lang="ts">
import { DirectiveBinding, ref, VNode } from 'vue';
let message = ref('123');
const changeMsg = () => {
message.value = '张三';
console.log('改变messag');
};
const vDebounce = {
created(el: HTMLElement, binding: DirectiveBinding, vnode, prevVnode) {
console.log(binding);
let timer: null | number = null;
el.addEventListener('click', () => {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
binding.value.fn(); // value = changeMsg
}, binding.value.time);
});
},
beforeMount() {},
mounted() {},
beforeUpdate() {},
updated() {},
beforeUnmount() {},
unmounted() {},
};
</script>
或者:这两个做对比,你会发现不一样,运用更灵活,上面说的理解更深刻
<template>
<button v-debounce="changeMsg">修改message</button>
</template>
<script setup lang="ts">
import { DirectiveBinding, ref, VNode } from 'vue';
let message = ref<string>('123');
const changeMsg = () => {
message.value = '张三';
console.log('改变messag');
};
const vDebounce = (el: HTMLElement, binding: DirectiveBinding) => {
let timer: null | number = null;
el.addEventListener('click', () => {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
binding.value(); // value = changeMsg
}, 1000);
});
};
</script>
v-throttle 节流
背景:在开发中,有些提交保存按钮有时候会在短时间内被点击多次,这样就会多次重复请求后端接口,造成数据的混乱,比如立即购买按钮,多次点击就会多次调用创建订单接口。
需求:防止按钮在短时间内被多次点击,使用节流函数限制规定时间内只能点击一次。
思路:
定义一个由开关(默认为开)控制是否执行的方法,第一次执行函数时将开关关闭,在规定时间内再调用该方法,则不会再次执行,直至规定时间过后开关打开。
将事件绑定在 click 方法上。
<template>
<div>{{ message }}</div>
<button v-throttle="{ fn: changeMsg, time: 3000 }">修改message</button>
</template>
<script setup lang="ts">
import { DirectiveBinding, ref } from 'vue';
let message = ref('123');
1;
const changeMsg = () => {
console.log('节流');
message.value = '节流';
};
let vThrottle = {
created(el: HTMLElement, binding: DirectiveBinding) {
console.log(binding);
console.log();
if (typeof binding.value.fn !== 'function') return;
el._flag = true; //开关默认为开
el._timer = null;
el.handler = function () {
if (!el._flag) return;
//执行之后开关关闭
el._flag && binding.value.fn();
el._flag = false;
if (el._timer !== null) {
clearTimeout(el._timer);
el._timer = null;
}
el._timer = setTimeout(() => {
el._flag = true; //三秒后开关开启
}, binding.value.time);
};
el.addEventListener('click', el.handler);
},
beforeMount() {},
mounted() {},
beforeUpdate() {},
updated() {},
beforeUnmount() {},
unmounted(el: HTMLElement, binding: DirectiveBinding) {
el.removeEventListener('click', el.handler);
},
};
</script>
v-drag 拖拽
页面中某个区域元素需要用户所以拖动,注意需要拖动的元素要给position:absolute等位属性;
<script>
写法,对比下面<script setup>
写法
<template>
<div class="drag" v-drag></div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
export default defineComponent({
directives: {
drag: {
created(el, binding) {
el.onmousedown = function (e) {
console.log(e);
//计算出元素距离上边和左边的距离(鼠标点击的位置-元素的位置)
//这个应该能理解吧
var disX = e.clientX - el.offsetLeft;
var disY = e.clientY - el.offsetTop;
let l;
let t;
document.onmousemove = function (e) {
//鼠标要按住不松开移动才行,松开就会触发onmouseup的事件
//计算出元素移动后的位置(鼠标位置-元素初始的disX/disY)
l = e.clientX - disX;
t = e.clientY - disY;
el.style.left = l + 'px';
el.style.top = t + 'px';
};
document.onmouseup = function (e) {
document.onmousemove = null;
document.onmouseup = null;
el.style.left = l + 'px';
el.style.top = t + 'px';
};
};
},
},
},
setup() {},
});
</script>
<style lang="less" scoped>
.drag {
width: 100px;
height: 100px;
background-color: pink;
position: absolute;
}
</style>
<script setup>
写法,对比上面<script >
写法
<template>
<div class="drag" v-drag></div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
const vDrag = {
created(el, binding) {
el.onmousedown = function (e) {
console.log(e);
//计算出元素距离上边和左边的距离(鼠标点击的位置-元素的位置)
//这个应该能理解吧
var disX = e.clientX - el.offsetLeft;
var disY = e.clientY - el.offsetTop;
let l;
let t;
document.onmousemove = function (e) {
//鼠标要按住不松开移动才行,松开就会触发onmouseup的事件
//计算出元素移动后的位置(鼠标位置-元素初始的disX/disY)
l = e.clientX - disX;
t = e.clientY - disY;
el.style.left = l + 'px';
el.style.top = t + 'px';
};
document.onmouseup = function (e) {
document.onmousemove = null;
document.onmouseup = null;
el.style.left = l + 'px';
el.style.top = t + 'px';
};
};
},
};
</script>
<style lang="less" scoped>
.drag {
width: 100px;
height: 100px;
background-color: pink;
position: absolute;
}
</style>