从简易到通用: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,但这里的hashCode
是FunctionProxy
实例的哈希值 ,而不是原始target
函数的哈希值。 -
每次调用
throttle()
时,都会new FunctionProxy(...)
,所以hashCode
变了 → 导致同一个函数节流/防抖不起作用。也就是说,现在这个实现可能根本没法限制住相同的函数,因为每次 key 都不一样。
-
如果想正确限制某个函数,key 应该跟
target
函数对象本身绑定,比如:iniString key = target.hashCode.toString();
或者用
identityHashCode(target)
。
2. throttle()
异步执行时会立即释放锁
-
这里:
vbnetawait target?.call(); _funcThrottle.remove(key);
如果
target
是异步的,执行完才释放锁没问题,但如果是同步的,就等于立刻释放了锁 → 节流时间其实为 0。- 一般节流会用
Timer
固定一段时间后再释放。
- 一般节流会用
3. 防抖(debounce
)调用时丢失参数
- 调用
target?.call()
时,没有把传入参数传递过去,所以如果原函数需要参数,这个防抖会执行失败或者不符合预期。- 正确做法应该是用
Function.apply(target!, args)
传参数。
- 正确做法应该是用
4. 类型安全缺失
-
现在所有地方都用
Function
,没有泛型支持,会导致编译器无法检查参数和返回类型,运行时才报错。 -
可以把
FunctionProxy
改成泛型,比如:scalaclass 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 直接支持 void
和 Future
函数 ✅ 任意参数支持 ------ 无参数、单参数、多参数全都直接调用 ✅ 可取消机制 ------ 支持在需要时主动终止计时器,避免内存泄漏和无意义调用 ✅ 调用姿势一致 ------ UI 层和业务层都能无缝使用
这样,在网络请求、表单提交、按钮点击等场景下,不管是 Flutter UI 事件还是业务逻辑函数,都能保持统一的调用体验,并且更健壮。