vue3中添加全局防抖指令
学习过程记录
1. 背景与需求
在开发Vue项目时,大家可能都会遇到一个常见的问题:用户频繁点击按钮触发多次API请求。这不仅会增加服务器负担,还可能导致数据混乱。为了解决这个问题,这里实现一个全局的防抖指令。
2. 防抖原理理解
首先,需要理解防抖的核心原理:
- 防抖(Debounce):指在事件被触发n秒后再执行回调,如果在这n秒内事件被再次触发,则重新计时
- 这种技术常用于处理用户输入、搜索、窗口调整等高频触发的事件
3. Vue3自定义指令学习
Vue3的自定义指令与Vue2有所不同,主要体现在:
生命周期钩子变化:
bind
改为mounted
update
与componentUpdated
合并为updated
unbind
改为beforeUnmount
指令的参数获取方式:
// 获取传入的回调函数 const func = binding.value; // 获取等待时间(arg),默认1000ms const wait = Number(binding.arg) || 1000; // 是否立即执行(修饰符) const immediate = binding.modifiers.immediate;
4. 实现思路
- 为使用指令的元素绑定一个点击事件处理函数
- 在处理函数中实现防抖逻辑
- 支持自定义延迟时间和立即执行选项
- 确保不同元素之间的防抖行为互不影响
5. 关键技术点
元素属性扩展:通过TypeScript扩展HTMLElement接口,为元素添加自定义属性
declare global { interface HTMLElement { __debounce_handler__?: EventListener; __debounce_timer__?: number | null; } }
闭包与事件处理:使用闭包创建独立的处理函数,保存状态
const createHandler = (func: Function, wait: number, immediate: boolean): EventListener => { return function(event: Event) { // 防抖逻辑 }; };
资源清理:在指令销毁前清理定时器和事件监听,防止内存泄漏
beforeUnmount(el: HTMLElement) { // 清除定时器和事件监听 }
6. 坑与解决方案
问题:直接传入带参数的函数会在页面加载时立即执行
<!-- 错误用法 --> <button v-debounce="selectTopic(item)">选择</button>
解决方案:使用箭头函数包裹
<!-- 正确用法 --> <button v-debounce="() => selectTopic(item)">选择</button>
问题:多个元素使用同一指令时防抖行为相互影响
解决方案:为每个元素创建独立的定时器和处理函数
// 为每个元素创建独立的定时器变量 el.__debounce_timer__ = null; // 为每个元素创建独立的处理函数 el.__debounce_handler__ = createHandler(func, wait, immediate);
7. 全局注册
最后,在main.ts中注册全局指令:
import { createApp } from 'vue'
import App from './App.vue'
import debounceDirective from './directives/debounce'
const app = createApp(App)
app.directive('debounce', debounceDirective)
app.mount('#app')
8. 后续优化
- 考虑扩展支持更多事件类型,如input、change等
- 添加节流(Throttle)功能,作为防抖的补充
- 增加更多调试选项,方便开发时排查问题
完整代码
/**
* 防抖指令
* 用法:v-debounce:500.immediate="handler"
* 参数:
* - 数字参数表示延迟时间,默认1000ms
* 修饰符:
* - immediate: 是否立即执行,默认为false
*
* 注意事项:
* - 在有参数的指令中,必须使用箭头函数包裹处理函数,否则会在页面加载时立即执行
* 错误用法: v-debounce="selectTopic(item)" // 会在元素加载时立即调用函数
* 正确用法: v-debounce="() => selectTopic(item)" // 只有点击时才会调用函数
* - 不要在同一个元素上多次使用此指令
* - 每个使用此指令的元素都有独立的防抖行为,互不影响
*
* 如果是不带参数的函数,正常使用指令即可:
* v-debounce.immediate="clickSend"
*/
// 扩展HTMLElement类型,添加自定义属性
declare global {
interface HTMLElement {
__debounce_handler__?: EventListener;
__debounce_timer__?: number | null;
}
}
// 定义Vue指令绑定的参数类型
interface DirectiveBinding {
value: Function;
arg?: string;
modifiers: {
[key: string]: boolean;
};
instance: any;
oldValue?: Function;
}
const debounceDirective = {
/**
* 指令挂载时调用
* @param {HTMLElement} el - 指令绑定的元素
* @param {DirectiveBinding} binding - 指令绑定的值
*/
mounted(el: HTMLElement, binding: DirectiveBinding) {
// 为每个元素创建独立的定时器变量
el.__debounce_timer__ = null;
// 创建处理函数
const createHandler = (func: Function, wait: number, immediate: boolean): EventListener => {
return function(event: Event) {
// 使用元素自身的定时器,确保每个元素有独立的定时器
const callNow = immediate && !el.__debounce_timer__;
// 清除此元素自己的定时器
if (el.__debounce_timer__) {
clearTimeout(el.__debounce_timer__);
}
// 设置新的定时器,存储在元素自己的属性中
el.__debounce_timer__ = window.setTimeout(() => {
if (!immediate) {
func.call(binding.instance, event);
}
el.__debounce_timer__ = null;
}, wait);
// 如果是立即执行,并且没有活动的定时器,则立即调用
if (callNow) {
func.call(binding.instance, event);
}
};
};
// 获取传入的回调函数
const func = binding.value;
// 获取等待时间(arg),默认 1000ms
const wait = Number(binding.arg) || 1000;
// 是否立即执行(修饰符)
const immediate = binding.modifiers.immediate;
// 为每个元素创建独立的处理函数
el.__debounce_handler__ = createHandler(func, wait, immediate);
// 添加点击事件监听器
el.addEventListener('click', el.__debounce_handler__);
},
/**
* 指令更新时调用,确保更新handler
* @param {HTMLElement} el - 指令绑定的元素
* @param {DirectiveBinding} binding - 指令绑定的值
*/
updated(el: HTMLElement, binding: DirectiveBinding) {
// 如果函数引用发生变化,需要更新处理函数
if (binding.value !== binding.oldValue && el.__debounce_handler__) {
// 移除旧的事件监听
el.removeEventListener('click', el.__debounce_handler__);
// 获取最新的配置
const func = binding.value;
const wait = Number(binding.arg) || 1000;
const immediate = binding.modifiers.immediate;
// 创建新的处理函数
const createHandler = (func: Function, wait: number, immediate: boolean): EventListener => {
return function(event: Event) {
const callNow = immediate && !el.__debounce_timer__;
if (el.__debounce_timer__) {
clearTimeout(el.__debounce_timer__);
}
el.__debounce_timer__ = window.setTimeout(() => {
if (!immediate) {
func.call(binding.instance, event);
}
el.__debounce_timer__ = null;
}, wait);
if (callNow) {
func.call(binding.instance, event);
}
};
};
// 更新处理函数
el.__debounce_handler__ = createHandler(func, wait, immediate);
// 重新添加事件监听
el.addEventListener('click', el.__debounce_handler__);
}
},
/**
* 指令卸载前调用
* @param {HTMLElement} el - 指令绑定的元素
*/
beforeUnmount(el: HTMLElement) {
// 清除定时器
if (el.__debounce_timer__) {
clearTimeout(el.__debounce_timer__);
el.__debounce_timer__ = null;
}
// 移除事件监听
if (el.__debounce_handler__) {
el.removeEventListener('click', el.__debounce_handler__);
// 删除附加的属性
el.__debounce_handler__ = undefined;
el.__debounce_timer__ = undefined;
}
}
}
export default debounceDirective;
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!