支持主动取消的防抖:两种 API 设计对比(写法一 vs 写法二)
本文对比两种「可取消防抖」的封装方式:Lodash 风格(单函数 + .cancel)与 双方法返回({ run, cancel }),并给出实现与选型建议。
一、为什么需要「可取消」的防抖?
防抖(debounce)大家都很熟:在连续触发时只执行最后一次。但有一种场景,仅「延迟执行」不够,还需要主动取消:
- 输入校验 + 异步请求 :用户输入金额 → 400ms 防抖后请求「计算手续费」。若用户在这 400ms 内把金额删成 0 或改成非法值,我们希望在校验失败时取消这次待执行的请求,而不是等 400ms 后仍用旧值或 0 去请求接口。
此时就需要:在错误分支里主动取消防抖 ,避免无效请求。
VueUse 的 useDebounceFn 没有暴露 .cancel(),所以我们可以自己封装一个「支持取消」的防抖,并在设计 API 时面临两种风格:写法一(单函数 + .cancel) 和 写法二(返回两个方法)。
二、写法一:Lodash 风格 ------ 一个函数 + .cancel
思路
返回一个函数 ,既可正常调用(触发防抖),又挂载 .cancel() 方法,用于取消当前等待中的执行。和 Lodash 的 debounce 返回的 API 一致。
实现
typescript
/**
* 类 lodash 的防抖,支持主动取消防抖(.cancel())
* @param fn 要防抖的函数
* @param delay 延迟毫秒数
* @returns 防抖后的函数,带 .cancel() 方法
*/
export function useDebounceWithCancel<T extends (...args: any[]) => any>(
fn: T,
delay: number,
): ((...args: Parameters<T>) => void) & { cancel: () => void } {
let timer: ReturnType<typeof setTimeout> | null = null;
function cancel() {
if (timer !== null) {
clearTimeout(timer);
timer = null;
}
}
function run(...args: Parameters<T>) {
cancel();
timer = setTimeout(() => {
fn(...args);
timer = null;
}, delay);
}
run.cancel = cancel;
return run;
}
使用方式
typescript
const debouncedCalculateFee = useDebounceWithCancel(calculateFee, 400);
// 触发防抖
debouncedCalculateFee();
// 在错误分支等场景下主动取消
debouncedCalculateFee.cancel();
特点
| 优点 | 缺点 |
|---|---|
| 只维护一个变量,心智负担小 | 类型要写交叉类型 Fn & { cancel: () => void } |
| 与 Lodash / 社区常见 API 一致 | 有人不习惯「函数上挂方法」 |
便于传递:把「防抖函数」当整体传参时,对方也能 .cancel() |
--- |
三、写法二:返回两个方法 ------ { run, cancel }
思路
不返回「带属性的函数」,而是直接返回两个方法:一个负责触发防抖(run),一个负责取消(cancel)。职责分离,一眼能看出是两个能力。
实现
typescript
/**
* 防抖,支持主动取消。返回两个方法:触发防抖 / 取消防抖
*/
export function useDebounceWithCancel<T extends (...args: any[]) => any>(
fn: T,
delay: number,
): { run: (...args: Parameters<T>) => void; cancel: () => void } {
let timer: ReturnType<typeof setTimeout> | null = null;
function cancel() {
if (timer !== null) {
clearTimeout(timer);
timer = null;
}
}
function run(...args: Parameters<T>) {
cancel();
timer = setTimeout(() => {
fn(...args);
timer = null;
}, delay);
}
return { run, cancel };
}
使用方式
typescript
const { run: debouncedCalculateFee, cancel: cancelCalculateFee } =
useDebounceWithCancel(calculateFee, 400);
// 触发防抖
debouncedCalculateFee();
// 取消防抖
cancelCalculateFee();
特点
| 优点 | 缺点 |
|---|---|
| 「触发」和「取消」职责分离,语义清晰 | 需要维护两个名字(或解构时起别名) |
类型简单,就是普通对象 { run, cancel } |
与 Lodash 等单函数 + .cancel 的形态不一致 |
| 解构时可自由命名(如 run → debouncedCalculateFee) | 若要把防抖「整体」传给子组件,需要传 run + cancel 两个 |
| 闭包逻辑与写法一完全一致 | --- |
四、闭包:两种写法是同一套逻辑
无论写法一还是写法二,防抖和取消能生效,靠的都是闭包:
run和cancel都在useDebounceWithCancel内部定义,共享同一份timer(以及fn、delay)。- 多次调用
run()会先cancel()再设新的setTimeout,所以「只执行最后一次」。 - 在任意时机调用
cancel()(或debouncedFn.cancel()),清掉的都是这一份timer。
所以:返回一个函数再挂 .cancel,还是返回 { run, cancel },闭包行为相同;差异只在 API 形态和调用方式。
五、对比与选型建议
| 维度 | 写法一(单函数 + .cancel) | 写法二({ run, cancel }) |
|---|---|---|
| 变量数量 | 一个 | 两个(或解构出两个名字) |
| 类型写法 | 交叉类型稍复杂 | 普通对象,简单 |
| 与 Lodash 一致性 | 一致 | 不一致 |
| 语义 | 「一个防抖函数,附带取消」 | 「两个独立能力」 |
| 传递/复用 | 传一个引用即可,对方可 .cancel() | 需传 run + cancel |
选型建议:
- 更看重和 Lodash / 社区习惯统一 ,或需要把「防抖」作为整体传递 (如传给子组件、工具函数)→ 优先 写法一。
- 更看重职责分离、类型简单、命名灵活 ,且多在本组件内使用 → 写法二 也很合适。
没有绝对优劣,按团队习惯和具体场景选即可。
六、小结
- 可取消防抖在「输入校验 + 延迟请求」场景里很实用,能避免无效请求。
- 写法一 :返回带
.cancel()的单个函数,Lodash 风格,便于传递和统一心智。 - 写法二 :返回
{ run, cancel },职责清晰,类型简单,闭包逻辑与写法一相同。 - 两种写法都依赖同一套闭包(共享
timer),可按团队偏好和场景在两种 API 间选择。
如果你也在做表单防抖、搜索防抖或类似「延迟执行 + 需要取消」的逻辑,不妨试试自己封装一个「支持 cancel 的防抖」,并在这两种 API 风格里选一种落地到项目里。