yx_async_throttle_flutter/lib/async_throttle.dart

148 lines
5.2 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
}
}
}