Compare commits
No commits in common. "master" and "2.0.0" have entirely different histories.
288
README.md
288
README.md
|
|
@ -93,20 +93,7 @@ void onSearchChanged(String query) async {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. 检查执行状态
|
### 3. 清除锁 (可选)
|
||||||
|
|
||||||
你可以随时检查某个任务是否正在执行中:
|
|
||||||
|
|
||||||
```dart
|
|
||||||
// 检查指定 tagId 是否有任务正在执行
|
|
||||||
bool isRunning = AsyncThrottle.instance.isExecuting('submit_order_btn');
|
|
||||||
|
|
||||||
if (isRunning) {
|
|
||||||
print('订单正在提交中,请稍候...');
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 清除锁 (可选)
|
|
||||||
|
|
||||||
在极少数情况下(例如用户退出登录时),你可能需要强制清除所有内部锁。
|
在极少数情况下(例如用户退出登录时),你可能需要强制清除所有内部锁。
|
||||||
|
|
||||||
|
|
@ -114,68 +101,6 @@ if (isRunning) {
|
||||||
AsyncThrottle.instance.clearAllLocks();
|
AsyncThrottle.instance.clearAllLocks();
|
||||||
```
|
```
|
||||||
|
|
||||||
> ⚠️ **注意**: `clearAllLocks()` 仅清除异步锁状态,不会影响 EasyThrottle/EasyDebounce 的内部时间窗口。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## API 参考
|
|
||||||
|
|
||||||
### `execute` 方法
|
|
||||||
|
|
||||||
```dart
|
|
||||||
Future<void> execute(
|
|
||||||
String tagId,
|
|
||||||
Future<void> Function() onExecute, {
|
|
||||||
Duration duration = const Duration(milliseconds: 300),
|
|
||||||
bool enableDebounce = false,
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
| 参数 | 类型 | 默认值 | 说明 |
|
|
||||||
|------|------|--------|------|
|
|
||||||
| `tagId` | `String` | 必填 | 任务的唯一标识符 |
|
|
||||||
| `onExecute` | `Future<void> Function()` | 必填 | 要执行的异步函数 |
|
|
||||||
| `duration` | `Duration` | `300ms` | 节流/防抖的时间间隔 |
|
|
||||||
| `enableDebounce` | `bool` | `false` | `false`=节流模式,`true`=防抖模式 |
|
|
||||||
|
|
||||||
### `executeSafe` 方法
|
|
||||||
|
|
||||||
```dart
|
|
||||||
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,
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
| 参数 | 类型 | 默认值 | 说明 |
|
|
||||||
|------|------|--------|------|
|
|
||||||
| `tagId` | `String` | 必填 | 任务的唯一标识符 |
|
|
||||||
| `onExecute` | `Future<void> 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` 库通常只根据 **时间** 来控制执行。
|
普通的 `Throttle` 库通常只根据 **时间** 来控制执行。
|
||||||
|
|
@ -191,217 +116,6 @@ void clearAllLocks()
|
||||||
|
|
||||||
> **💡 最佳实践**: 如果在 `onExecute` 函数内部再添加 **Loading 蒙层**(UI 阻断),配合本插件即可实现 **“三层防护”**(UI 蒙层 + 时间策略 + 异步锁),彻底杜绝任何异常场景下的重复操作。
|
> **💡 最佳实践**: 如果在 `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<void> 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)。
|
本库底层的时间策略依赖于 [easy_debounce](https://pub.dev/packages/easy_debounce)。
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT License
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue