diff --git a/README.md b/README.md index 6c291af..32eeaad 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,20 @@ void onSearchChanged(String query) async { } ``` -### 3. 清除锁 (可选) +### 3. 检查执行状态 + +你可以随时检查某个任务是否正在执行中: + +```dart +// 检查指定 tagId 是否有任务正在执行 +bool isRunning = AsyncThrottle.instance.isExecuting('submit_order_btn'); + +if (isRunning) { + print('订单正在提交中,请稍候...'); +} +``` + +### 4. 清除锁 (可选) 在极少数情况下(例如用户退出登录时),你可能需要强制清除所有内部锁。 @@ -101,6 +114,68 @@ void onSearchChanged(String query) async { AsyncThrottle.instance.clearAllLocks(); ``` +> ⚠️ **注意**: `clearAllLocks()` 仅清除异步锁状态,不会影响 EasyThrottle/EasyDebounce 的内部时间窗口。 + +--- + +## API 参考 + +### `execute` 方法 + +```dart +Future execute( + String tagId, + Future Function() onExecute, { + Duration duration = const Duration(milliseconds: 300), + bool enableDebounce = false, +}) +``` + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `tagId` | `String` | 必填 | 任务的唯一标识符 | +| `onExecute` | `Future Function()` | 必填 | 要执行的异步函数 | +| `duration` | `Duration` | `300ms` | 节流/防抖的时间间隔 | +| `enableDebounce` | `bool` | `false` | `false`=节流模式,`true`=防抖模式 | + +### `executeSafe` 方法 + +```dart +Future executeSafe( + String tagId, + Future Function() onExecute, { + Duration duration = const Duration(milliseconds: 300), + bool enableDebounce = false, + void Function(Object error, StackTrace stackTrace)? onError, +}) +``` + +| 参数 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| `tagId` | `String` | 必填 | 任务的唯一标识符 | +| `onExecute` | `Future Function()` | 必填 | 要执行的异步函数 | +| `duration` | `Duration` | `300ms` | 节流/防抖的时间间隔 | +| `enableDebounce` | `bool` | `false` | `false`=节流模式,`true`=防抖模式 | +| `onError` | `Function?` | `null` | 错误回调,用于日志/上报 | + +### `isExecuting` 方法 + +```dart +bool isExecuting(String tagId) +``` + +检查指定 `tagId` 的任务是否正在执行中。 + +### `clearAllLocks` 方法 + +```dart +void clearAllLocks() +``` + +强制清除所有异步锁和等待中的 Completer。 + +--- + ## 核心优势:为什么需要异步锁? 普通的 `Throttle` 库通常只根据 **时间** 来控制执行。 @@ -116,6 +191,217 @@ AsyncThrottle.instance.clearAllLocks(); > **💡 最佳实践**: 如果在 `onExecute` 函数内部再添加 **Loading 蒙层**(UI 阻断),配合本插件即可实现 **“三层防护”**(UI 蒙层 + 时间策略 + 异步锁),彻底杜绝任何异常场景下的重复操作。 +--- + +## 更多使用示例 + +### 表单多按钮场景 + +当页面有多个按钮时,使用不同的 `tagId` 确保它们互不影响: + +```dart +class OrderPage extends StatelessWidget { + void onSave() { + AsyncThrottle.instance.executeSafe('order_save', () async { + await saveOrder(); + }); + } + + void onSubmit() { + AsyncThrottle.instance.executeSafe('order_submit', () async { + await submitOrder(); + }); + } + + void onCancel() { + AsyncThrottle.instance.executeSafe('order_cancel', () async { + await cancelOrder(); + }); + } +} +``` + +### 配合 GetX/Provider 状态管理 + +```dart +class OrderController extends GetxController { + final isLoading = false.obs; + + Future submitOrder() async { + await AsyncThrottle.instance.execute( + 'submit_order', + () async { + isLoading.value = true; + try { + await api.submitOrder(); + Get.snackbar('成功', '订单提交成功'); + } finally { + isLoading.value = false; + } + }, + ); + } +} +``` + +### 列表项点击防重 + +```dart +ListView.builder( + itemBuilder: (context, index) { + final item = items[index]; + return ListTile( + onTap: () { + // 使用 item.id 作为唯一标识,确保不同列表项互不影响 + AsyncThrottle.instance.executeSafe( + 'list_item_${item.id}', + () async { + await navigateToDetail(item); + }, + ); + }, + ); + }, +) +``` + +### 实时输入验证(防抖) + +```dart +TextField( + onChanged: (value) { + AsyncThrottle.instance.execute( + 'username_validation', + () async { + final isAvailable = await api.checkUsername(value); + setState(() { + usernameError = isAvailable ? null : '用户名已被占用'; + }); + }, + enableDebounce: true, + duration: const Duration(milliseconds: 500), + ); + }, +) +``` + +--- + +## 注意事项与最佳实践 + +### ✅ 推荐做法 + +1. **使用有意义的 tagId** + ```dart + // ✅ 好 + 'submit_order_page_main' + 'search_product_list' + + // ❌ 不好 + 'btn1' + 'click' + ``` + +2. **在 onTap/onPressed 中使用 executeSafe** + ```dart + // ✅ 推荐:不会影响 UI + onTap: () { + AsyncThrottle.instance.executeSafe('action', doSomething); + } + + // ⚠️ 需要注意:异常可能变成 unhandled async error + onTap: () async { + await AsyncThrottle.instance.execute('action', doSomething); + } + ``` + +3. **合理设置 duration** + - 按钮点击:`300ms ~ 500ms` + - 搜索防抖:`300ms ~ 800ms` + - 实时验证:`500ms ~ 1000ms` + +### ⚠️ 注意事项 + +1. **tagId 全局唯一**:不同功能使用不同的 tagId,避免意外阻塞 + +2. **单例模式**:`AsyncThrottle.instance` 是全局单例,状态跨页面共享 + +3. **页面销毁时的处理**:如果需要在页面销毁时取消未完成的任务,可以在 `dispose` 中调用: + ```dart + @override + void dispose() { + // 可选:清除该页面相关的锁 + // 注意:这会清除所有锁,请谨慎使用 + // AsyncThrottle.instance.clearAllLocks(); + super.dispose(); + } + ``` + +4. **异常处理**:`execute` 会传播异常,`executeSafe` 会吞掉异常并调用 `onError` + +--- + +## 与其他方案的对比 + +| 特性 | yx_async_throttle | 普通 Throttle | 手动锁变量 | +|------|-------------------|---------------|-----------| +| 时间策略 | ✅ | ✅ | ❌ | +| 异步锁 | ✅ | ❌ | ✅ | +| 弱网保护 | ✅ | ❌ | ✅ | +| 内存安全 | ✅ | ✅ | ⚠️ 需自行处理 | +| 防抖模式 | ✅ | ✅ | ❌ | +| 代码简洁 | ✅ | ✅ | ❌ | + +--- + +## 测试覆盖 + +本库已通过 **75 项** 全面的单元测试,覆盖: + +- ✅ 单例模式 +- ✅ 节流/防抖核心功能 +- ✅ 异常处理与传播 +- ✅ 并发场景(100+ 次快速调用) +- ✅ 内存安全(无泄漏) +- ✅ 重入防护(无死锁) +- ✅ 边界条件(极端参数) +- ✅ 真实业务场景模拟 + +详细测试报告请参阅 [TEST_REPORT.md](./TEST_REPORT.md)。 + +--- + +## 常见问题 (FAQ) + +### Q: 为什么我的第二次点击没有执行? + +A: 这正是本库的设计目的。检查以下两点: +1. 是否在 `duration` 时间窗口内? +2. 上一次任务是否还在执行中?(使用 `isExecuting` 检查) + +### Q: 如何让同一按钮在不同页面独立工作? + +A: 使用包含页面标识的 tagId: +```dart +'submit_order_${widget.pageId}' +``` + +### Q: executeSafe 和 execute 有什么区别? + +A: +- `execute`: 异常会向外抛出,适合可以 await 的场景 +- `executeSafe`: 异常被内部捕获,通过 onError 回调处理,适合 onTap 等无法 await 的场景 + +### Q: clearAllLocks 后为什么还是不能立即执行? + +A: `clearAllLocks` 只清除异步锁,EasyThrottle 的时间窗口仍然有效。需要等待时间窗口过期,或使用不同的 tagId。 + +--- + ## 其他信息 本库底层的时间策略依赖于 [easy_debounce](https://pub.dev/packages/easy_debounce)。 + +## License + +MIT License