Dart 异步编程
异步编程是现代应用开发中最核心的能力之一。
无论是网络请求、文件读写还是数据库操作,都需要异步处理来避免阻塞主线程。
本章介绍 Future 的概念、async/await 语法以及如何处理异步错误。
为什么需要异步编程
先理解同步和异步的区别。
同步代码按顺序一行一行执行,上一行完成之前下一行不会开始。
如果某个操作需要等待(如网络请求),整个程序就会被卡住。
实例
对比同步和异步代码的执行效果:
String fetchDataSync() {
// 同步代码:假设这里要等待 2 秒
// 在这 2 秒内,整个程序什么都做不了
sleep(Duration(seconds: 2)); // 阻塞式等待
return '数据获取完成';
}
// 异步版本:不阻塞主线程
Future<String> fetchDataAsync() async {
// 异步等待:这 2 秒内程序可以继续执行其他任务
await Future.delayed(Duration(seconds: 2));
return '数据获取完成';
}
void main() async {
print('开始同步获取数据...');
var syncResult = fetchDataSync();
print('同步结果: $syncResult');
print('(注意:同步期间程序被卡住了)');
print('\n开始异步获取数据...');
print('发起请求后,程序继续执行其他任务...');
var asyncResult = await fetchDataAsync();
print('异步结果: $asyncResult');
print('(异步期间程序没有被卡住)');
}
开始同步获取数据... 同步结果: 数据获取完成 (注意:同步期间程序被卡住了) 开始异步获取数据... 发起请求后,程序继续执行其他任务... 异步结果: 数据获取完成 (异步期间程序没有被卡住)
在 Dart 中,所有 I/O 操作(文件、网络、数据库)都应该使用异步方式。同步 I/O 会阻塞整个 Isolate,在 Flutter 应用中会导致 UI 卡顿。
Future 的概念
Future
它像一个承诺:我现在没有结果,但将来一定会给你一个 T 类型的值(或者一个错误)。
Future 有三种状态:
| 状态 | 说明 |
|---|---|
| 未完成(Uncompleted) | 异步操作还在进行中 |
| 完成且有值(Completed with data) | 操作成功,Future 携带了结果值 |
| 完成但有错误(Completed with error) | 操作失败,Future 携带了错误信息 |
实例
创建和使用 Future:
print('程序开始');
// 创建一个 Future
Future<String> future = Future(() {
// 这个函数会在未来某个时刻执行
return 'RUNOOB 异步结果';
});
print('Future 已创建,但还没完成');
// 注册回调:当 Future 完成时执行
future.then((result) {
print('收到结果: $result');
});
print('程序继续执行...');
}
程序开始 Future 已创建,但还没完成 程序继续执行... 收到结果: RUNOOB 异步结果
注意输出的顺序:先打印"程序继续执行",后打印"收到结果"。
这说明 Future 的回调是异步执行的,不会阻塞后续代码。
async / await 语法
async 和 await 是 Dart 处理异步操作的核心语法。
async 标记一个函数为异步函数,await 等待一个 Future 完成并获取其结果。
实例
// 异步函数的返回类型必须是 Future<T>
Future<String> fetchUserName(int id) async {
// await 等待 Future 完成,并获取其值
// 在等待期间,当前函数暂停执行,但不会阻塞其他代码
await Future.delayed(Duration(seconds: 1));
return '用户$id';
}
Future<int> fetchUserAge(int id) async {
await Future.delayed(Duration(milliseconds: 500));
return 20 + id;
}
// 顺序执行多个异步操作
Future<void> printUserInfo(int id) async {
print('开始获取用户信息...');
// 逐个等待:先获取名称,再获取年龄
var name = await fetchUserName(id);
print('获取到名称: $name');
var age = await fetchUserAge(id);
print('获取到年龄: $age');
print('RUNOOB 用户: $name, 年龄: $age');
}
// 并发执行多个异步操作
Future<void> printUserInfoParallel(int id) async {
print('开始并发获取用户信息...');
// 同时发起两个请求,不互相等待
var nameFuture = fetchUserName(id);
var ageFuture = fetchUserAge(id);
// 等待两个请求都完成
var results = await Future.wait([nameFuture, ageFuture]);
var name = results[0] as String;
var age = results[1] as int;
print('RUNOOB 用户(并发): $name, 年龄: $age');
}
void main() async {
// 顺序执行(总时间 ≈ 1s + 0.5s = 1.5s)
await printUserInfo(1);
print('---');
// 并发执行(总时间 ≈ max(1s, 0.5s) = 1s)
await printUserInfoParallel(2);
}
开始获取用户信息... 获取到名称: 用户1 获取到年龄: 21 RUNOOB 用户: 用户1, 年龄: 21 --- 开始并发获取用户信息... RUNOOB 用户(并发): 用户2, 年龄: 22
如果多个异步操作之间没有依赖关系,应该使用 Future.wait 让它们并发执行,而不是逐个 await。这能显著提升性能——总耗时等于最慢的操作,而非所有操作之和。
async/await 的核心规则
| 规则 | 说明 |
|---|---|
| async 函数返回 Future | 即使函数返回 T,async 后会自动包装为 Future<T> |
| await 只能在 async 函数中使用 | 普通函数不能使用 await |
| await 暂停当前函数 | 但不阻塞其他代码的执行 |
| await 获取 Future 的结果 | 如果 Future 有错误,await 会抛出异常 |
then / catchError 链式调用
除了 async/await,Dart 还支持使用 then() 和 catchError() 进行链式调用。
这是传统的 Promise 风格写法。
实例
Future<String> fetchData(String url) {
return Future.delayed(Duration(seconds: 1), () {
if (url.isEmpty) {
throw Exception('URL 不能为空');
}
return '来自 $url 的数据';
});
}
void main() {
print('开始请求...');
// then / catchError 链式调用
fetchData('https://runoob.com/api')
.then((data) {
// 请求成功
print('获取到数据: $data');
return '处理后的: $data'; // 返回值会传递给下一个 then
})
.then((processed) {
// 处理上一个 then 的返回值
print(processed);
})
.catchError((error) {
// 统一捕获链中的任何错误
print('请求出错: $error');
})
.whenComplete(() {
// 无论成功还是失败都会执行(类似 finally)
print('请求结束(无论成功或失败)');
});
print('请求已发出,程序继续...');
}
开始请求... 请求已发出,程序继续... 获取到数据: 来自 https://runoob.com/api 的数据 处理后的: 来自 https://runoob.com/api 的数据 请求结束(无论成功或失败)
async/await vs then/catchError 如何选择
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 线性异步流程 | async/await | 代码像同步一样清晰 |
| 并发多个请求 | async/await + Future.wait | 语义明确 |
| 链式数据转换 | then() | 每个 then 做一步转换,链式清晰 |
| 单一回调 | then() | 比 async/await 更简洁 |
大多数情况下推荐 async/await,它的可读性更好。但 then() 在简单的链式处理中依然有价值。两者可以混用,但不建议——一个函数要么用 async/await,要么用 then/catchError,混用会让代码难以理解。
处理异步错误
异步操作中可能发生错误,需要用 try-catch 来处理。
async/await 让错误处理变得和同步代码一样简单。
实例
Future<String> fetchUserProfile(int userId) async {
await Future.delayed(Duration(seconds: 1));
if (userId <= 0) {
throw Exception('无效的用户 ID: $userId');
}
if (userId == 404) {
throw HttpException('用户不存在');
}
return '用户 $userId 的个人资料';
}
// 自定义异常
class HttpException implements Exception {
final String message;
HttpException(this.message);
@override
String toString() => 'HttpException: $message';
}
Future<void> loadUserProfile(int userId) async {
print('正在加载用户 $userId 的资料...');
try {
var profile = await fetchUserProfile(userId);
print('加载成功: $profile');
} on HttpException catch (e) {
// 按类型捕获特定的异步错误
print('HTTP 错误: $e');
print('提示用户:请检查用户是否存在');
} on Exception catch (e) {
// 捕获其他异常
print('一般错误: $e');
} finally {
print('加载操作结束');
}
}
void main() async {
await loadUserProfile(1); // 成功
print('---');
await loadUserProfile(404); // HttpException
print('---');
await loadUserProfile(-1); // 一般 Exception
}
正在加载用户 1 的资料... 加载成功: 用户 1 的个人资料 加载操作结束 --- 正在加载用户 404 的资料... HTTP 错误: HttpException: 用户不存在 提示用户:请检查用户是否存在 加载操作结束 --- 正在加载用户 -1 的资料... 一般错误: Exception: 无效的用户 ID: -1 加载操作结束
async 函数中的错误处理规则:
- 如果 async 函数中抛出异常,该异常会被自动包装到 Future 中
- 调用者可以用 try-catch 捕获 await 的异常
- 如果使用 then(),异常会传递到 catchError()
- 未捕获的异步异常不会导致程序崩溃,但会被视为未处理的 Future 错误
永远不要忽略异步错误。每个 async 函数调用都应该有对应的错误处理。未处理的异步错误在开发模式下会打印警告,在生产环境中可能被静默忽略,导致难以排查的 bug。
