1、封装自定义指令
// utils/debounce.js
import { ref, isRef, unref, nextTick } from 'vue'
export const debounce = {
mounted(el, binding) {
// 获取指令参数和修饰符
const { value, arg, modifiers } = binding
// 配置选项
const options = {
delay: modifiers.limit ? 500 : 3000, // 防抖延迟时间(默认3秒)
showLoading: !modifiers.noloading, // 是否显示加载状态
event: arg || 'click', // 事件类型(默认click)
immediate: !!modifiers.immediate, // 是否立即执行第一次点击
resetOnError: !!modifiers.reset // 错误时是否重置防抖状态
}
// 响应式加载状态
const isLoading = ref(false)
// 查找ElementPlus Button内部的button元素
const findButtonEl = () => {
if (el.tagName.toLowerCase() === 'button') return el
return el.querySelector('button.el-button') || el
}
// 更新加载状态
const updateLoadingState = (loading) => {
isLoading.value = loading
const buttonEl = findButtonEl()
if (!buttonEl || !options.showLoading) return
if (loading) {
buttonEl.classList.add('is-loading')
// 添加加载图标(如果没有)
if (!buttonEl.querySelector('.el-icon-loading')) {
const icon = document.createElement('i')
icon.className = 'el-icon-loading'
buttonEl.prepend(icon)
// 调整原有内容的位置
if (buttonEl.querySelector('.el-button__content')) {
buttonEl.querySelector('.el-button__content').classList.add('ml-1')
}
}
} else {
buttonEl.classList.remove('is-loading')
const icon = buttonEl.querySelector('.el-icon-loading')
if (icon) icon.remove()
if (buttonEl.querySelector('.el-button__content')) {
buttonEl.querySelector('.el-button__content').classList.remove('ml-1')
}
}
}
// 处理不同类型的绑定值
let handler
let lastCallTime = 0
if (typeof value === 'function') {
// 情况1:直接绑定函数(无参数)
handler = () => {
if (isLoading.value) return
const now = Date.now()
const allowCall = options.immediate || (now - lastCallTime > options.delay)
if (allowCall) {
lastCallTime = now
updateLoadingState(true)
return Promise.resolve(value())
.catch(error => {
if (options.resetOnError) {
// 错误时重置防抖状态
lastCallTime = 0
}
throw error
})
.finally(() => {
// 使用setTimeout确保延迟后才重置加载状态
setTimeout(() => updateLoadingState(false), options.delay)
})
}
}
} else if (typeof value === 'object' && value !== null) {
// 情况2:通过对象传递函数和参数
const { handler: fn, params = [] } = value
if (typeof fn !== 'function') {
console.error('v-debounce对象格式错误:handler必须是函数')
return
}
handler = () => {
if (isLoading.value) return
const now = Date.now()
const allowCall = options.immediate || (now - lastCallTime > options.delay)
if (allowCall) {
lastCallTime = now
updateLoadingState(true)
// 解析参数(支持ref和普通值)
const resolvedParams = params.map(param =>
isRef(param) ? unref(param) : param
)
return Promise.resolve(fn(...resolvedParams))
.catch(error => {
if (options.resetOnError) {
lastCallTime = 0
}
throw error
})
.finally(() => {
setTimeout(() => updateLoadingState(false), options.delay)
})
}
}
} else {
console.error('v-debounce指令需要绑定函数或函数配置对象')
return
}
// 绑定事件监听器
el.addEventListener(options.event, handler)
// 保存引用以便在unmounted时移除
el.__debounceHandler = handler
el.__debounceEvent = options.event
},
unmounted(el) {
// 清理事件监听器
if (el.__debounceHandler) {
el.removeEventListener(el.__debounceEvent, el.__debounceHandler)
delete el.__debounceHandler
delete el.__debounceEvent
}
}
}
2、全局注册指令
import { createApp } from 'vue';
import ElementPlus from 'element-plus';
import 'element-plus/dist/index.css';
import App from './App.vue';
import { debounce } from './utils/debounce';
const app = createApp(App);
app.use(ElementPlus);
app.directive('debounce', debounce);
app.mount('#app');
3、两种调用方式示例
方式 1:直接调用函数(无参数)
<el-button v-debounce="submitForm" type="primary">
提交
</el-button>
const submitForm = () => {
console.log('提交表单');
return api.submit().then(() => {
console.log('提交成功');
});
};
方式 2:带参数调用函数
<el-button v-debounce="{ handler: submitWithForm, params: [formRef] }" type="primary">
带参提交
</el-button>
const formRef = ref(null);
const submitWithForm = (form) => {
return new Promise((resolve) => {
form.validate((valid) => {
if (valid) {
api.submit(form.model).then(resolve);
}
});
});
};
4、高级用法和修饰符
自定义防抖时间
<el-button v-debounce.limit="submitQuickly" type="primary">
快速提交
</el-button>
禁用加载状态
<el-button v-debounce.noloading="submitSilently" type="primary">
静默提交
</el-button>
立即执行第一次点击
<el-button v-debounce.immediate="submitNow" type="primary">
立即提交
</el-button>
错误时重置防抖状态
<el-button v-debounce.reset="submitWithRetry" type="primary">
可重试提交
</el-button>