主动取消的防抖

支持主动取消的防抖:两种 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 风格里选一种落地到项目里。


相关推荐
剪刀石头布啊8 小时前
vue-react为何要虚拟dom
前端
剪刀石头布啊8 小时前
ahooks常用的一些库,我们使用一下
前端
剪刀石头布啊8 小时前
从git更新本地提交记录到更新已经推送到远端记录
前端
云浪9 小时前
Go 并发实战:从零实现一个高并发安全的任务调度器
javascript·后端·go
Flynt10 小时前
axios 被投毒半年后,我把 npm 的"安全防线"实测了一遍:最信任的 SHA512 是安慰剂
前端·npm·node.js
excel11 小时前
Base62 和 Base64 有什么区别?
前端
风骏时光牛马12 小时前
大模型底层架构与AI源码深度剖析
前端
掘金挖土13 小时前
前端手摸手跑路之 AI 应用开发(九)
前端·后端
中文呀16 小时前
三维 GIS 离线缓存实战:逆向 CesiumOfflineCache,并亲手为它补上"自动更新"能力
前端