148 lines
5.2 KiB
Dart
148 lines
5.2 KiB
Dart
import 'dart:async';
|
||
|
||
import 'package:easy_debounce/easy_debounce.dart';
|
||
import 'package:easy_debounce/easy_throttle.dart';
|
||
|
||
/// 异步防抖/节流工具类
|
||
class AsyncThrottle {
|
||
// 私有构造函数,防止外部实例化
|
||
AsyncThrottle._();
|
||
|
||
// 懒汉式单例 - 使用 late final 延迟初始化
|
||
static final AsyncThrottle _instance = AsyncThrottle._();
|
||
|
||
// 获取单例实例
|
||
static AsyncThrottle get instance => _instance;
|
||
|
||
// 存储异步任务锁状态
|
||
final Map<String, bool> _asyncLocks = {};
|
||
|
||
// 存储防抖模式下的 Completer,确保被取消的任务能正确结束 await
|
||
final Map<String, Completer<void>> _debounceCompleters = {};
|
||
|
||
/// 异步执行方法 - 防止弱网重复点击 (无 Loading UI)
|
||
///
|
||
/// 结合了 时间策略(防抖/节流)和 异步任务状态锁。
|
||
/// 只有当满足以下两个条件时才会执行:
|
||
/// 1. 当前没有正在执行的同名任务 (Task Lock - 解决弱网长耗时问题)
|
||
/// 2. 满足时间策略 (Time Policy - 解决快速连点问题)
|
||
///
|
||
/// [tagId]: 唯一标识符
|
||
/// [onExecute]: 要执行的异步方法
|
||
/// [duration]: 时间间隔,默认 300ms
|
||
/// [enableDebounce]: 是否启用防抖模式。
|
||
/// - true: 使用防抖 (Debounce) - 延迟执行,最后一次点击生效(适合搜索、输入)
|
||
/// - false: 使用节流 (Throttle) - 立即执行,忽略后续点击(默认,适合按钮点击)
|
||
Future<void> execute(
|
||
String tagId,
|
||
Future<void> Function() onExecute, {
|
||
Duration duration = const Duration(milliseconds: 300),
|
||
bool enableDebounce = false,
|
||
}) async {
|
||
// 1. 检查异步锁 (防止上一个请求未回来时重复点击)
|
||
// 无论是防抖还是节流,只要任务还在执行中,都不应重入
|
||
if (isExecuting(tagId)) return;
|
||
|
||
if (enableDebounce) {
|
||
// === 防抖模式 (Debounce) ===
|
||
|
||
// 关键修复:检查是否有正在等待的防抖请求。
|
||
// 如果有,说明它被本次新请求“顶掉”了(EasyDebounce 会取消上一个 Timer)。
|
||
// 我们需要手动完成它,否则上一次的 await 将永远挂起,导致内存泄漏。
|
||
if (_debounceCompleters.containsKey(tagId)) {
|
||
_completeDebounce(tagId);
|
||
}
|
||
|
||
final completer = Completer<void>();
|
||
_debounceCompleters[tagId] = completer;
|
||
|
||
EasyDebounce.debounce(tagId, duration, () async {
|
||
// 双重检查:确保 Completer 还在(防止极端并发情况)
|
||
if (!_debounceCompleters.containsKey(tagId)) return;
|
||
|
||
// 防抖触发时,再次检查锁
|
||
if (isExecuting(tagId)) {
|
||
_completeDebounce(tagId);
|
||
return;
|
||
}
|
||
|
||
_asyncLocks[tagId] = true;
|
||
try {
|
||
await onExecute();
|
||
} catch (e, stack) {
|
||
// 这里必须捕获并转发到 execute() 返回的 Future。
|
||
// 否则异常会在 Timer 回调中变成未捕获异步异常(unhandled async error),影响 UI。
|
||
_completeDebounce(tagId, error: e, stackTrace: stack);
|
||
} finally {
|
||
_asyncLocks.remove(tagId);
|
||
_completeDebounce(tagId); // 任务结束,通知 await 返回
|
||
}
|
||
});
|
||
|
||
await completer.future;
|
||
} else {
|
||
// === 节流模式 (Throttle) - 默认 ===
|
||
// 立即执行,并在 duration 内忽略后续调用
|
||
// throttle 返回 true 表示被节流(忽略),false 表示获得了执行权
|
||
final isThrottled = EasyThrottle.throttle(tagId, duration, () {});
|
||
|
||
// 如果被节流了,直接返回
|
||
if (isThrottled) return;
|
||
|
||
// 获得执行权,加锁并执行
|
||
_asyncLocks[tagId] = true;
|
||
try {
|
||
await onExecute();
|
||
} finally {
|
||
_asyncLocks.remove(tagId);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// UI 安全版:用于 onTap/onPressed 等无法 await 的场景。
|
||
///
|
||
/// - 永远不会向外抛异常(避免影响 UI / 触发全局未捕获异常)
|
||
/// - 可通过 [onError] 收集日志/上报/Toast
|
||
Future<void> executeSafe(
|
||
String tagId,
|
||
Future<void> Function() onExecute, {
|
||
Duration duration = const Duration(milliseconds: 300),
|
||
bool enableDebounce = false,
|
||
void Function(Object error, StackTrace stackTrace)? onError,
|
||
}) {
|
||
return execute(tagId, onExecute, duration: duration, enableDebounce: enableDebounce).catchError((
|
||
Object e,
|
||
StackTrace s,
|
||
) {
|
||
onError?.call(e, s);
|
||
});
|
||
}
|
||
|
||
/// 辅助方法:安全地完成并移除 Completer
|
||
void _completeDebounce(String tagId, {Object? error, StackTrace? stackTrace}) {
|
||
if (_debounceCompleters.containsKey(tagId)) {
|
||
final completer = _debounceCompleters[tagId];
|
||
if (completer != null && !completer.isCompleted) {
|
||
if (error != null) {
|
||
completer.completeError(error, stackTrace);
|
||
} else {
|
||
completer.complete();
|
||
}
|
||
}
|
||
_debounceCompleters.remove(tagId);
|
||
}
|
||
}
|
||
|
||
/// 检查任务是否正在执行
|
||
bool isExecuting(String tagId) => _asyncLocks[tagId] ?? false;
|
||
|
||
/// 强制取消所有锁(慎用)
|
||
void clearAllLocks() {
|
||
_asyncLocks.clear();
|
||
// 清理时把所有等待的 Future 也都释放掉,防止外部 await 永久挂起
|
||
for (var key in _debounceCompleters.keys.toList()) {
|
||
_completeDebounce(key);
|
||
}
|
||
}
|
||
}
|