主动取消的防抖

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


相关推荐
REDcker1 小时前
Cesium三维WebGIS入门详解
前端·gis·web·cesium·webgis
0暗影流光02 小时前
十倍效能提升——Web 基础研发体系的建立
前端
前端一课2 小时前
用Agent Skill重构代码:300行缩至30行的实战教程
前端
腻害兔2 小时前
【若依项目-产品经理视角】RuoYi-Vue-Pro 源码拆解:IM 即时通讯模块,一个被低估的「全功能聊天系统」
java·前端·vue.js·产品经理·ai编程
奎叔2 小时前
Flutter 异常处理体系:从捕获、转换到恢复
前端
心念枕惊2 小时前
.NET CORE 授权进阶-角色、策略与动态权限实现
java·前端·.netcore
小徐_23332 小时前
寓言故事一则:狗猛酒酸
前端·ai编程
xiaobaoyu3 小时前
vue2+element开发中问题和解决方式
前端
KaMeidebaby3 小时前
卡梅德生物技术快报 | 核酸适配体文库测序:核酸适配体文库测序的技术原理、实验流程与数据解析
前端·网络·数据库·人工智能·算法
Hilaku3 小时前
为什么在 2026 年,MPA(多页面应用)正在悄悄复辟?
前端·javascript·程序员