背景
例如我们有个功能,点击保存请求接口操作。
<template>
<el-form :model="form" label-width="120px">
<el-form-item label="Activity name">
<el-input v-model="form.name" />
</el-form-item>
<el-form-item label="Activity form">
<el-input v-model="form.desc" type="textarea" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">确认</el-button>
<el-button>取消</el-button>
</el-form-item>
</el-form>
</template>
<script setup>
import { reactive } from 'vue';
const form = reactive({
name: '',
desc: '',
});
const onSubmit = () => {
console.log('submit!');
};
</script>
如果连续点击保存按钮,会一直请求接口,可能会造成服务器压力,如果点击过快接口有可能会报错。
思路
首先我们想一下该怎么解决这个问题?
- 使用防抖函数,点击调用事件处理
- 控制点击行为,某个时间段不能点击
- 接口加锁控制请求
- 写个公用的组件,点击事件控制
思路1和思路3每个点击事件都要做处理,处理起来有点麻烦, 相对思路2和思路4来说,控制点击行为还是比较简单点,而且方便。
实现
这里我们用自定义指令实现
知识点
工欲善其事,必先利其器
首先我们看下自定义指令的一些知识点
指令钩子#
一个指令的定义对象可以提供几种钩子函数 (都是可选的):
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) {}
}
钩子参数#
指令的钩子会传递以下几种参数:
-
el:指令绑定到的元素。这可以用于直接操作 DOM。 -
binding:一个对象,包含以下属性。value:传递给指令的值。例如在v-my-directive="1 + 1"中,值是2。oldValue:之前的值,仅在beforeUpdate和updated中可用。无论值是否更改,它都可用。arg:传递给指令的参数 (如果有的话)。例如在v-my-directive:foo中,参数是"foo"。modifiers:一个包含修饰符的对象 (如果有的话)。例如在v-my-directive.foo.bar中,修饰符对象是{ foo: true, bar: true }。instance:使用该指令的组件实例。dir:指令的定义对象。
-
vnode:代表绑定元素的底层 VNode。 -
prevNode:之前的渲染中代表指令所绑定元素的 VNode。仅在beforeUpdate和updated钩子中可用。
实战
1. 创建指令
我们创建一个button指令, 监听点击事件,点击设置按钮不可点击,800ms后恢复,防止重复点击。
这里设置V开头,是方便使用<script setup>
// directives.js
export const VButton = {
mounted: (el, binding) => {
el.handler = function () {
el.classList.add('is-disabled');
el.disabled = true;
setTimeout(() => {
el.classList.remove('is-disabled');
el.disabled = false;
}, 800);
};
el.addEventListener('click', el.handler);
},
unmounted: (el) => {
el.removeEventListener('click', el.handler);
},
};
2. 组件中使用
<template>
<el-form :model="form" label-width="120px">
<el-form-item label="Activity name">
<el-input v-model="form.name" />
</el-form-item>
<el-form-item label="Activity form">
<el-input v-model="form.desc" type="textarea" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit" v-button>确认</el-button>
<el-button>取消</el-button>
</el-form-item>
</el-form>
</template>
<script setup>
import { reactive } from 'vue';
import { VButton } from './directives'
const form = reactive({
name: '',
desc: '',
});
const onSubmit = () => {
console.log('submit!');
};
3. 全局使用
// main.js
import { createApp } from 'vue';
import App from './App.vue';
import { VButton } from '@/utils/directives'
const app = createApp(App);
app.directive('button', VButton);
现在再使用按钮点击,就不会连续点击连续请求接口的情况了,是不是很简单。
优化
功能实现了,下一步就是优化功能和代码。
首先我们先来想一下:
- 是不是可以动态设置防抖时间?
- 是不是可以点击一次加锁,不能点击,重新刷新页面才能点击?once
- 其他...
这里只是给你一个思路,具体业务具体安排。
我们来实现问题1和问题2,该怎么传参是我们所需要的。
binding:一个对象,包含以下属性。
value:传递给指令的值。例如在v-my-directive="1 + 1"中,值是2。oldValue:之前的值,仅在beforeUpdate和updated中可用。无论值是否更改,它都可用。arg:传递给指令的参数 (如果有的话)。例如在v-my-directive:foo中,参数是"foo"。modifiers:一个包含修饰符的对象 (如果有的话)。例如在v-my-directive.foo.bar中,修饰符对象是{ foo: true, bar: true }。instance:使用该指令的组件实例。dir:指令的定义对象。
再来看一下,这里有个参数,我们可以通过binding获取参数.
- 我们先通过
value来实现传参
<template>
<el-button type="primary" @click="onSubmit" v-button="{ delay:1000 }">
确认
</el-button>
</template>
打印结果看下,可以看到value对象中delay,这就好办了。
我们修改下directives.js
// directives.js
export const VButton = {
mounted: (el, binding) => {
el.handler = function () {
const { delay = 800 } = binding.value || {};
el.classList.add('is-disabled');
el.disabled = true;
setTimeout(() => {
el.classList.remove('is-disabled');
el.disabled = false;
}, delay);
};
el.addEventListener('click', el.handler);
},
unmounted: (el) => {
el.removeEventListener('click', el.handler);
},
};
- 然后我们使用修饰符实现问题2,只能点击一次
once
<template>
<el-button type="primary" @click="onSubmit" v-button.once="{ delay:1000 }">
确认
</el-button>
</template>
我们再次修改下directives.js
// directives.js
export const VButton = {
mounted: (el, binding) => {
el.handler = function () {
const { delay = 800 } = binding.value || {};
el.classList.add('is-disabled');
el.disabled = true;
const { once } = binding.modifiers;
if (once) return;
setTimeout(() => {
el.classList.remove('is-disabled');
el.disabled = false;
}, delay);
};
el.addEventListener('click', el.handler);
},
unmounted: (el) => {
el.removeEventListener('click', el.handler);
},
};
是不是感觉很简单,这里只是简单的做个扩展,其他的自定义指令类似。