主动取消的防抖

支持主动取消的防抖:两种 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 两个
闭包逻辑与写法一完全一致 ---

四、闭包:两种写法是同一套逻辑

无论写法一还是写法二,防抖和取消能生效,靠的都是闭包

  • runcancel 都在 useDebounceWithCancel 内部定义,共享同一份 timer(以及 fndelay)。
  • 多次调用 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 风格里选一种落地到项目里。


相关推荐
百度地图汽车版2 小时前
【AI地图 Tech说】第九期:让智能体拥有记忆——打造千人千面的小度想想
前端·后端
臣妾没空2 小时前
Elpis 全栈框架:从构建到发布的完整实践总结
前端·后端
H5开发新纪元2 小时前
Nginx 部署 Vue3 项目完整指南
前端·javascript·面试
决斗小饼干2 小时前
跨语言移植手记:把 TypeScript 的 Codex SDK 请进 .NET 世界
前端·javascript·typescript
小码哥_常2 小时前
Android Intent.setAction失效报错排查与修复全方案
前端
进击的尘埃2 小时前
Vitest 浏览器模式:别再用 jsdom 骗自己了
javascript
bluceli2 小时前
JavaScript模块化深度解析:从CommonJS到ES Modules的演进之路
前端·javascript
前端人类学2 小时前
前端输入框禁用:disabled、readonly 与.prop (‘disabled‘, true) 完全解析
前端·javascript
优秀稳妥的JiaJi3 小时前
分享一篇后台管理系统的通用skills
前端·vue.js·前端框架