Dart Completer (任务完成器) 详解

Dart Completer (任务完成器) 详解

1. 什么是 Completer?

Completer 是 Dart 中用于手动创建和管理 Future 的工具。

  • 普通 Future:由 async 函数自动管理,一旦开始执行便无法在外部手动干预其完成时机。
  • Completer:提供了一个“手动挡”的 Future。它允许你创建一个 Future,并把“何时完成”以及“以什么结果完成”的控制权掌握在自己手中。

简单来说,当你需要返回一个 Future 给调用者,但这个 Future 的结果需要在稍后的某个时刻(例如回调函数触发时)才能确定,这时就是使用 Completer 的最佳时机。


2. 核心结构

Completer 主要由两个核心成员组成:

  1. completer.future

    • 作用:这是暴露给外部等待的对象。
    • 行为:调用者 await 这个属性时,程序会暂停,直到你手动调用 complete 方法。
  2. completer.complete(value)

    • 作用:这是“触发完成”的开关。
    • 行为:一旦调用,所有正在等待 future 的代码会立即收到结果并继续执行。

3. 生活中的类比:取餐器

想象你在快餐店点餐的过程:

  1. 创建 Completer:你点完餐,服务员给了你一个 **取餐器 (Completer)**。
  2. await future:你拿着取餐器找座位坐下,看着取餐器 **等待 (await)**。
  3. 异步操作:后厨正在 **制作汉堡 (异步任务)**。
  4. 调用 complete:汉堡做好了,服务员按下柜台上的发射器 **(completer.complete())**。
  5. 完成:你手中的取餐器响了,你起身去取餐。

4. 基本用法示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import 'dart:async';

void main() async {
print('1. 主程序开始');

// 1. 创建一个 Completer
final completer = Completer<String>();

// 2. 启动一个异步任务,把 completer 传进去
print('2. 启动异步任务...');
simulateAsyncWork(completer);

// 3. 主程序在这里暂停,等待 completer 被完成
print('3. 等待结果...');
String result = await completer.future;

// 5. 收到结果,继续执行
print('5. 收到结果: $result');
}

void simulateAsyncWork(Completer<String> completer) {
// 模拟耗时操作
Future.delayed(Duration(seconds: 3), () {
print('4. 任务完成,手动调用 complete');

// 4. 手动完成这个 Future,传入结果数据
completer.complete('任务成功数据');
});
}

执行顺序:

  1. 1. 主程序开始
  2. 2. 启动异步任务...
  3. 3. 等待结果...
  4. (等待3秒)
  5. 4. 任务完成,手动调用 complete
  6. 5. 收到结果: 任务成功数据

5. 常见应用场景

场景一:将回调 (Callback) 转换为 Future

这是 Completer 最经典的应用场景。很多老旧的库或底层 API 仍然使用回调函数风格,我们可以用 Completer 将其包装成现代的 async/await 风格。

旧式 API (Callback 风格):

1
2
3
void oldDownload(String url, Function(String) onSuccess, Function(String) onError) {
// ... 内部通过回调通知结果 ...
}

使用 Completer 包装 (Future 风格):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Future<String> downloadAsync(String url) {
final completer = Completer<String>();

oldDownload(url,
(data) {
// 成功回调 -> 完成 Future
completer.complete(data);
},
(error) {
// 失败回调 -> 让 Future 抛出异常
completer.completeError(error);
}
);

return completer.future; // 返回 future 供外部 await
}

// 调用方式变得非常整洁:
// try {
// String data = await downloadAsync('...');
// } catch (e) {
// // 处理错误
// }

场景二:解决并发请求合并 (Request Deduplication)

这正是我们在 CleanupManager 中解决的问题。当多个页面同时请求同一个耗时资源时,我们不希望发起多次重复请求,而是希望后来的请求“搭便车”。

实现模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class DataManager {
Completer<void>? _loadingCompleter;

Future<void> loadData() async {
// 1. 检查锁:如果有正在进行的任务...
if (_loadingCompleter != null) {
// 2. 搭便车:直接等待现有的任务完成
await _loadingCompleter!.future;
return;
}

// 3. 加锁:我是第一个,我来创建一个新的任务管理器
_loadingCompleter = Completer<void>();

try {
// 4. 执行真正的耗时请求
await api.fetchData();
} finally {
// 5. 解锁:无论成功失败,通知所有等待者(包括我自己和搭便车的人)
_loadingCompleter?.complete();
_loadingCompleter = null;
}
}
}

效果:

  • 调用 A 进来 -> 创建 Completer -> 开始请求。
  • 调用 B 进来 -> 发现 Completer 存在 -> await 它。
  • 调用 C 进来 -> 发现 Completer 存在 -> await 它。
  • 请求结束 -> 调用 complete() -> A, B, C 同时收到通知并继续执行。

6. 总结

特性 普通 Future Completer
创建方式 async 函数自动返回 Completer() 构造函数
完成控制 函数 return 时自动完成 开发者手动调用 complete()
适用场景 大多数标准异步逻辑 桥接回调 API、并发控制、自定义异步流程

掌握 Completer,意味着你从“只会使用异步”进阶到了“能够设计和控制异步流程”的水平。