Dart 语言的异步编程 Future 和 Stream
单线程模型与异步任务
Dart 是一门单线程语言,所有代码默认运行在一个独立的事件循环(event loop)上。如果某个操作需要等待较长时间(如读取文件、网络请求),同步执行会阻塞整个线程,导致界面卡死。异步编程正是为了解决这类问题而生:把耗时操作交给系统执行,当前线程继续处理其他任务,待操作完成后再通过回调、Future 或 Stream 取回结果。
Dart 提供两种核心异步抽象:Future 表示一个即将完成(或失败)的单个值,Stream 则表示一串连续的异步事件或数据。
Future:代表未来的单个值
Future<T> 就像一个承诺:它现在可能没有值,但会在未来某个时刻提供类型为 T 的结果,或者抛出一个错误。任何返回 Future 的函数都会被立即挂起,将控制权交还事件循环,直到操作完成。
创建 Future
最直接的方式是使用 Future 构造函数:
Future<String> fetchData() {
return Future.delayed(Duration(seconds: 2), () => '数据加载完成');
}
Future.delayed 模拟了一个延时操作。你也可以用 Future.value() 立即返回一个已知值,或用 Future.error() 立即返回一个错误。
消费 Future:then、catchError、whenComplete
获取 Future 结果最基础的方法是注册回调:
fetchData()
.then((value) => print('成功:$value'))
.catchError((error) => print('失败:$error'))
.whenComplete(() => print('无论成败都会执行'));
then:成功时触发,接收结果值。catchError:发生异常时触发,可接收异常对象。whenComplete:总是执行,常用于清理资源。
链式调用可以让多个异步操作串行执行,每个 then 的返回值会被包装成新的 Future。
async 与 await:像同步代码一样写异步
async 和 await 是 Dart 最优雅的异步处理方式。在函数体前添加 async 关键字,函数会自动返回一个 Future。内部使用 await 可以暂停当前函数的执行,直到 Future 完成,然后直接取得结果。
Future<void> loadData() async {
try {
String data = await fetchData();
print('数据:$data');
} catch (e) {
print('出错:$e');
} finally {
print('操作结束');
}
}
await只能在async函数中使用。- 使用
try-catch-finally即可处理异常,就像处理同步代码一样。 - 多个 Future 可以同时发起请求,再用
await一起等待,提升并发性能:
Future<void> loadMultiple() async {
final futureA = fetchDataFromA(); // 立即启动
final futureB = fetchDataFromB(); // 立即启动
final resultA = await futureA;
final resultB = await futureB;
}
Stream:连续的数据流
Stream<T> 表示一系列异步事件或数据。它适合处理连续到达的值,比如用户点击、文件读取进度、WebSocket 消息等。Stream 可以是“单订阅”(single-subscription)或“广播”(broadcast)模式。
创建 Stream
Dart 提供了多种创建 Stream 的方式:
1. 使用异步生成器 async*
这是最直观的方法,使用 yield 逐步发出数据:
Stream<int> countStream(int max) async* {
for (int i = 1; i <= max; i++) {
await Future.delayed(Duration(seconds: 1));
yield i;
}
}
2. 使用 StreamController
手动控制数据的添加、错误和关闭:
StreamController<String> controller = StreamController<String>();
// 添加数据
controller.add('第一条消息');
controller.add('第二条消息');
// 添加错误
controller.addError(Exception('模拟错误'));
// 关闭流
controller.close();
// 获取 Stream
Stream<String> stream = controller.stream;
对于广播流,使用 StreamController.broadcast() 允许多个监听者。
监听 Stream:listen
Stream 激活前不会执行任何计算。调用 listen 会返回一个 StreamSubscription 对象,可用于控制订阅(暂停、恢复、取消)。
StreamSubscription<int> subscription = countStream(5).listen(
(data) {
print('收到:$data');
},
onError: (error) {
print('错误:$error');
},
onDone: () {
print('流已关闭');
},
);
你可以在适当时候调用 subscription.pause()、subscription.resume() 或 subscription.cancel()。
使用 await for 循环
如果只需要顺序处理流中的每个事件,可以使用 await for:
Future<void> processStream() async {
await for (var value in countStream(3)) {
print('处理:$value');
}
print('流处理结束');
}
注意:await for 会暂停函数的执行直到每次有新事件到达,适合单订阅流。
Stream 常用操作符
Stream 提供了大量类似集合的操作方法,可以帮助你转换、过滤、组合数据流。
| 方法 | 说明 |
|---|---|
map<T> |
将每个事件转换为另一种类型 |
where |
过滤掉不符合条件的事件 |
expand<T> |
将每个事件扩展为多个事件 |
take |
只取前 n 个事件 |
skip |
跳过前 n 个事件 |
distinct |
过滤掉连续重复的事件 |
transform |
应用一个 StreamTransformer |
handleError |
拦截并处理错误,阻止流关闭 |
timeout |
为每个事件设置超时 |
asyncMap |
对每个事件执行异步转换 |
asyncExpand |
异步扩展事件 |
链式调用示例
Stream<int> numbers = Stream.periodic(Duration(seconds: 1), (i) => i + 1);
numbers
.where((n) => n % 2 == 0) // 只保留偶数
.map((n) => '偶数:$n') // 转换为字符串
.take(3) // 只取前3个
.listen(print);
错误处理
Future 错误处理
使用 try-catch 或 catchError 都可以捕获 Future 链中的错误。未捕获的异常会导致程序崩溃。一定要处理错误,尤其是在结合 await 时,记得包裹在 try 块中。
Stream 错误处理
Stream 中的错误会通过 StreamSubscription 的 onError 回调处理,或者被 handleError 拦截。如果没有错误处理,单订阅流会直接关闭,广播流则会继续接受后续事件。
stream.handleError((error) {
print('捕获到错误:$error');
}).listen(null);
Future 与 Stream 的相互转换
有些场景需要从一个列表构建流,或者把流的全部事件收集成一个列表。Dart 提供了内置的支持。
Stream.fromFuture:将单个 Future 包装成一个流。Stream.fromFutures:从一组 Future 创建流。Stream.fromIterable:从集合创建同步数据流。Future.wait:并发执行多个 Future,并返回所有结果的列表。Stream.toList():将流的所有事件收集到一个列表(返回Future<List<T>>)。Stream.toSet()、Stream.length等。
实际应用场景
网络请求(Future)
Future<http.Response> fetchPost() async {
final response = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/posts/1'));
if (response.statusCode == 200) {
return response.body;
} else {
throw Exception('加载失败');
}
}
用户交互防抖(Stream)
对按钮点击事件流进行过滤,防止快速多次触发。
button.onClick
.debounceTime(Duration(milliseconds: 300))
.listen((_) => submitForm());
文件读取进度(Stream)
通过 Stream 逐块读取大文件,实时更新进度条。
总结
- Future 处理“一次性的异步返回值”,用
then或await消费结果。 - Stream 处理“多次推送的异步事件序列”,用
listen或await for订阅。 - 始终处理错误,避免未捕获的异常导致应用中断。
- 熟练使用
async/await可以让异步代码清晰易读,接近同步编码体验。 - 掌握 Stream 的操作符,能极大提高数据处理效率。
理解 Future 和 Stream 是 Dart 异步编程的基石,也是 Flutter 应用开发中不可或缺的技能。通过大量实践,你将逐渐习惯事件循环的工作方式,编写出高效、流畅的异步代码。