现在位置: 首页 > Dart 教程 > 正文

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:

void main() {
  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 完成并获取其结果。

实例

// async 关键字标记函数为异步函数
// 异步函数的返回类型必须是 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。