从简易到通用: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 事件还是业务逻辑函数,都能保持统一的调用体验,并且更健壮。

相关推荐
tangweiguo030519874 小时前
Dart 单例模式:工厂构造、静态变量与懒加载
flutter
叽哥5 小时前
flutter学习第 11 节:状态管理进阶:Provider
android·flutter·ios
猪哥帅过吴彦祖7 小时前
Flutter AnimatedList 完全指南:打造流畅的动态列表体验
flutter
天岚7 小时前
温故知新-WidgetsBinding
flutter
叽哥7 小时前
flutter学习第 10 节:表单与输入
android·flutter·ios
卢叁11 小时前
Flutter开发环境安装指南
前端·flutter
TralyFang12 小时前
InheritedWidget是如何建立依赖关系的
flutter
Levi147986545928912 小时前
flutter_flavorizr 多渠道打包、多环境打包利器,不需要再一个个手动配置了
flutter
LinXunFeng1 天前
Flutter - 使用本地 DevTools 验证 SVG 加载优化
flutter·性能优化·svg