从简易到通用:FunctionThrottleDebounce 升级全记录(支持同步 & 异步、任意参数、可取消)

从简易到通用:FunctionThrottleDebounce 升级全记录(支持同步 & 异步、任意参数、可取消)

目的:由于原来的扩展存在潜在的问题,为了方便后期维护,于是进行优化!

项目中原来的版本:

dart 复制代码
import 'dart:async';

import 'package:flutter/material.dart';

extension FunctionExtension on Function {
  VoidCallback throttle() {
    return FunctionProxy(this).throttle;
  }

  VoidCallback throttleWithTimeout({int? timeout}) {
    return FunctionProxy(this, timeout: timeout).throttleWithTimeout;
  }

  VoidCallback debounce({int? timeout}) {
    return FunctionProxy(this, timeout: timeout).debounce;
  }
}

class FunctionProxy {
  static final Map<String, bool> _funcThrottle = {};
  static final Map<String, Timer> _funcDebounce = {};
  final Function? target;

  final int timeout;

  FunctionProxy(this.target, {int? timeout}) : timeout = timeout ?? 500;

  void throttle() async {
    String key = hashCode.toString();
    bool enable = _funcThrottle[key] ?? true;
    if (enable) {
      _funcThrottle[key] = false;
      try {
        await target?.call();
      } catch (e) {
        rethrow;
      } finally {
        _funcThrottle.remove(key);
      }
    }
  }

  void throttleWithTimeout() {
    String key = hashCode.toString();
    bool enable = _funcThrottle[key] ?? true;
    if (enable) {
      _funcThrottle[key] = false;
      Timer(Duration(milliseconds: timeout), () {
        _funcThrottle.remove(key);
      });
      target?.call();
    }
  }

  void debounce() {
    String key = hashCode.toString();
    Timer? timer = _funcDebounce[key];
    timer?.cancel();
    timer = Timer(Duration(milliseconds: timeout), () {
      Timer? t = _funcDebounce.remove(key);
      t?.cancel();
      target?.call();
    });
    _funcDebounce[key] = timer;
  }
}

潜在问题

这样的实现有几个可能踩坑的地方:

1. hashCode 用法有风险

  • hashCode.toString() 作为 Map key,但这里的 hashCodeFunctionProxy 实例的哈希值 ,而不是原始 target 函数的哈希值。

  • 每次调用 throttle() 时,都会 new FunctionProxy(...),所以 hashCode 变了 → 导致同一个函数节流/防抖不起作用。

    也就是说,现在这个实现可能根本没法限制住相同的函数,因为每次 key 都不一样。

  • 如果想正确限制某个函数,key 应该跟 target 函数对象本身绑定,比如:

    ini 复制代码
    String key = target.hashCode.toString();

    或者用 identityHashCode(target)


2. throttle() 异步执行时会立即释放锁

  • 这里:

    vbnet 复制代码
    await target?.call();
    _funcThrottle.remove(key);

    如果 target 是异步的,执行完才释放锁没问题,但如果是同步的,就等于立刻释放了锁 → 节流时间其实为 0。

    • 一般节流会用 Timer 固定一段时间后再释放。

3. 防抖(debounce)调用时丢失参数

  • 调用 target?.call() 时,没有把传入参数传递过去,所以如果原函数需要参数,这个防抖会执行失败或者不符合预期。
    • 正确做法应该是用 Function.apply(target!, args) 传参数。

4. 类型安全缺失

  • 现在所有地方都用 Function,没有泛型支持,会导致编译器无法检查参数和返回类型,运行时才报错。

  • 可以把 FunctionProxy 改成泛型,比如:

    scala 复制代码
    class FunctionProxy<T extends Function> {
      final T target;
      ...
    }

5. 全局 Map 会导致内存泄漏

  • _funcThrottle_funcDebounce 是静态的,理论上应该在合适的时机清理。
  • 如果的函数是一次性的,但 key 永远不清除,就会一直占用内存。

修改后的版本:

dart 复制代码
import 'dart:async';

/**
    使用示例和场景说明:

    1. 普通节流 throttle
    -------------------------
    用法:
    final throttledFn = someFunction.throttle(milliseconds: 800);
    throttledFn(); // 触发调用,800ms 内再次调用会被忽略

    场景:
    - 按钮防止重复点击提交
    - 避免短时间内多次触发耗资源事件

    2. 立即执行节流 throttleWithTimeout
    -------------------------
    用法:
    final throttledImmediateFn = someFunction.throttleWithTimeout(milliseconds: 800);
    throttledImmediateFn(); // 立即执行,之后 800ms 内忽略调用

    场景:
    - 搜索输入框实时搜索,首次立即发请求,短时间内不重复请求
    - UI 动画首次立即触发,防止连续快速触发

    3. 可取消节流 throttleCancellable
    -------------------------
    用法:
    final (fn, cancel) = someFunction.throttleCancellable(milliseconds: 800);
    fn(); // 调用
    cancel(); // 取消冷却状态,允许立即再次调用

    场景:
    - 网络请求节流,用户操作中断时提前允许重新触发
    - 复杂多步骤任务,可根据业务主动取消节流限制

    4. 普通防抖 debounce
    -------------------------
    用法:
    final debouncedFn = someFunction.debounce(milliseconds: 500);
    debouncedFn(args); // 多次调用,只有最后一次在 500ms 后执行

    场景:
    - 输入框实时搜索,用户停止输入后再发请求
    - 调整窗口大小,等调整完成后才触发计算

    5. 立即执行防抖 debounceImmediate
    -------------------------
    用法:
    final debouncedImmediateFn = someFunction.debounceImmediate(milliseconds: 500);
    debouncedImmediateFn(); // 第一次立即执行,之后 500ms 内忽略调用

    场景:
    - 表单验证第一次立即提示,短期内不重复弹窗
    - 重要事件第一次响应,之后忽略

    6. 可取消防抖 debounceCancellable
    -------------------------
    用法:
    final (fn, cancel) = someFunction.debounceCancellable(milliseconds: 500);
    fn(args); // 调用
    cancel(); // 取消未触发的调用

    场景:
    - 实时搜索中,用户切换页面取消搜索请求
    - 输入框清空时取消等待执行的搜索请求

    -------------------------
    所有方法支持同步和异步函数,支持无参数和多参数调用。

    使用时建议根据场景选择合适方法,常见选择:
    - 普通按钮防重复点击用 throttle
    - 输入框实时搜索用 debounce
    - 需要立即触发且节流用 throttleWithTimeout
    - 需要手动控制取消时用可取消版本

 */

/// 扩展 Function,提供通用节流、防抖方法
/// 特点:
/// 1. 支持任意参数(无参数、单参数、多参数)
/// 2. 支持同步 & 异步函数
/// 3. 所有方法返回的函数可直接作为事件回调(免 as 转换)
/// 4. 部分方法支持手动取消
extension FunctionThrottleDebounce on Function {
  //============================ 节流类 ============================

  /// 1. 普通节流(延迟执行版)
  /// 调用场景:按钮点击、短时间内重复触发的事件
  /// 原理:第一次触发立即执行,之后在 [milliseconds] 毫秒内屏蔽再次触发
  /// 不支持手动取消,适用于生命周期短的场景(如单次页面会话)
  F throttle<F extends Function>({int milliseconds = 500}) {
    bool enable = true;

    return (([dynamic args]) async {
      if (!enable) return;
      enable = false;

      try {
        if (args == null) {
          await Function.apply(this, []);
        } else if (args is List) {
          await Function.apply(this, args);
        } else {
          await Function.apply(this, [args]);
        }
      } finally {
        Timer(Duration(milliseconds: milliseconds), () {
          enable = true;
        });
      }
    }) as F;
  }

  /// 2. 立即执行节流(带超时节流)
  /// 调用场景:需要第一次立刻触发,之后进入冷却期
  /// 例如:搜索请求第一次就发,后面一段时间内不再发
  F throttleWithTimeout<F extends Function>({int milliseconds = 500}) {
    bool enable = true;

    return (([dynamic args]) async {
      if (!enable) return;
      enable = false;

      if (args == null) {
        await Function.apply(this, []);
      } else if (args is List) {
        await Function.apply(this, args);
      } else {
        await Function.apply(this, [args]);
      }

      Timer(Duration(milliseconds: milliseconds), () {
        enable = true;
      });
    }) as F;
  }

  /// 3. 可取消节流(延迟执行版)
  /// 调用场景:与 [throttle] 类似,但可在外部取消冷却状态
  /// 适用于任务提前结束、需要立即允许下一次执行的场景
  /// 调用返回值: (wrappedFn, cancelFn)
  (F, void Function()) throttleCancellable<F extends Function>(
      {int milliseconds = 500}) {
    bool enable = true;
    Timer? timer;

    void cancel() {
      timer?.cancel();
      enable = true;
    }

    final wrapped = (([dynamic args]) async {
      if (!enable) return;
      enable = false;

      try {
        if (args == null) {
          await Function.apply(this, []);
        } else if (args is List) {
          await Function.apply(this, args);
        } else {
          await Function.apply(this, [args]);
        }
      } finally {
        timer = Timer(Duration(milliseconds: milliseconds), () {
          enable = true;
        });
      }
    }) as F;

    return (wrapped, cancel);
  }

  //============================ 防抖类 ============================

  /// 4. 普通防抖(延迟执行版)
  /// 调用场景:输入框实时搜索、窗口调整大小事件
  /// 原理:重复调用会重置定时器,直到最后一次调用才执行
  F debounce<F extends Function>({int milliseconds = 500}) {
    Timer? timer;

    return (([dynamic args]) {
      timer?.cancel();

      timer = Timer(Duration(milliseconds: milliseconds), () async {
        if (args == null) {
          await Function.apply(this, []);
        } else if (args is List) {
          await Function.apply(this, args);
        } else {
          await Function.apply(this, [args]);
        }
      });
    }) as F;
  }

  /// 5. 立即执行防抖(Leading Debounce)
  /// 调用场景:第一次调用立即执行,后续调用在 [milliseconds] 内忽略
  /// 例如:表单验证第一次立即提示,短期内不重复提示
  F debounceImmediate<F extends Function>({int milliseconds = 500}) {
    Timer? timer;
    bool firstCall = true;

    return (([dynamic args]) {
      if (firstCall) {
        firstCall = false;
        if (args == null) {
          Function.apply(this, []);
        } else if (args is List) {
          Function.apply(this, args);
        } else {
          Function.apply(this, [args]);
        }
      }
      timer?.cancel();
      timer = Timer(Duration(milliseconds: milliseconds), () {
        firstCall = true;
      });
    }) as F;
  }

  /// 6. 可取消防抖(延迟执行版)
  /// 调用场景:与 [debounce] 类似,但可在外部取消未触发的调用
  /// 适用于用户取消输入、提前终止操作的场景
  /// 调用返回值: (wrappedFn, cancelFn)
  (F, void Function()) debounceCancellable<F extends Function>(
      {int milliseconds = 500}) {
    Timer? timer;

    void cancel() {
      timer?.cancel();
    }

    final wrapped = (([dynamic args]) {
      timer?.cancel();

      timer = Timer(Duration(milliseconds: milliseconds), () async {
        if (args == null) {
          await Function.apply(this, []);
        } else if (args is List) {
          await Function.apply(this, args);
        } else {
          await Function.apply(this, [args]);
        }
      });
    }) as F;

    return (wrapped, cancel);
  }
}

升级后的版本 : ✅ 同步 + 异步通用 ------ 同一套 API 直接支持 voidFuture 函数 ✅ 任意参数支持 ------ 无参数、单参数、多参数全都直接调用 ✅ 可取消机制 ------ 支持在需要时主动终止计时器,避免内存泄漏和无意义调用 ✅ 调用姿势一致 ------ UI 层和业务层都能无缝使用

这样,在网络请求、表单提交、按钮点击等场景下,不管是 Flutter UI 事件还是业务逻辑函数,都能保持统一的调用体验,并且更健壮。

相关推荐
程序员老刘2 小时前
一杯奶茶钱,PicGo + 阿里云 OSS 搭建永久稳定的个人图床
flutter·markdown
奋斗的小青年!!5 小时前
OpenHarmony Flutter 拖拽排序组件性能优化与跨平台适配指南
flutter·harmonyos·鸿蒙
小雨下雨的雨6 小时前
Flutter 框架跨平台鸿蒙开发 —— Stack 控件之三维层叠艺术
flutter·华为·harmonyos
行者967 小时前
OpenHarmony平台Flutter手风琴菜单组件的跨平台适配实践
flutter·harmonyos·鸿蒙
小雨下雨的雨9 小时前
Flutter 框架跨平台鸿蒙开发 —— Flex 控件之响应式弹性布局
flutter·ui·华为·harmonyos·鸿蒙系统
cn_mengbei9 小时前
Flutter for OpenHarmony 实战:CheckboxListTile 复选框列表项详解
flutter
cn_mengbei9 小时前
Flutter for OpenHarmony 实战:Switch 开关按钮详解
flutter
奋斗的小青年!!9 小时前
OpenHarmony Flutter实战:打造高性能订单确认流程步骤条
flutter·harmonyos·鸿蒙
Coder_Boy_9 小时前
Flutter基础介绍-跨平台移动应用开发框架
spring boot·flutter
cn_mengbei9 小时前
Flutter for OpenHarmony 实战:Slider 滑块控件详解
flutter