> 技术文档 > Dart 异步编程之 Future 详解

Dart 异步编程之 Future 详解


1. 前言

Dart 是一种单线程模型的编程语言,这意味着所有的 Dart 代码在同一个线程上运行。在一些情况下,例如进行网络请求、读取大文件或执行复杂计算时,如果采用同步方式,主线程将会被阻塞,导致应用程序无法响应用户的操作,出现卡顿现象。而异步编程则允许程序在等待这些耗时操作完成的同时,继续执行其他任务,从而提高程序的响应性和用户体验。

2. 理解 Future

2.1 官方描述

用专业术语来说,Future是Future类的对象,其表示一个T类型的异步操作结果。如果异步操作不需要结果,则Future的类型可为Future。当一个返回Future对象的函数被调用时,会发生两件事:

  • 将函数操作列入队列等待执行并返回一个未完成的Future对象。
  • 不久后当函数操作执行完成,Future对象变为完成并携带一个值或一个错误。
2.2 通俗理解

我们可以把Future理解为一个装有数据的 “盒子”。一开始执行一个异步请求会返回一个Future“盒子”,此时 “盒子” 处于关闭状态,代表异步操作尚未完成。然后程序会继续执行下面的其他代码,不会被阻塞。等到过了一会异步请求结果返回时,这个Future“盒子” 就会打开。如果请求成功,“盒子” 里就装着请求结果的值;如果请求失败,“盒子” 里装着的则是请求异常。

因此,Future就会有三种状态分别是:

  • 未完成的状态 (Uncompleted):“盒子” 处于关闭状态;
  • 完成带有值的状态 (Completed with a value):“盒子” 打开并且正常返回结果状态;
  • 完成带有异常的状态 (Completed with a error):“盒子” 打开并且失败返回异常状态。

实际上从Future源码角度分析,总共有 5 种状态,其中比较关键且易于理解的是上述三种状态,另外两种状态相对较少直接接触,它们主要用于Future内部状态流转的更细致描述 。

3. 创建 Future

3.1 Future.value

使用Future.value可以创建一个已经完成的Future,并返回给定的值。

Future<int> createCompletedFuture() { return Future.value(42);}

在上述代码中,createCompletedFuture函数返回一个Future,该Future已经完成,其结果为42。

3.2 Future.delayed

Future.delayed用于创建一个在指定延迟时间后完成的Future。

Future<String> delayedFuture() { return Future.delayed(Duration(seconds: 2), () => \'延迟两秒后的结果\');}

在这个例子中,delayedFuture函数返回一个Future,该Future会在延迟 2 秒后完成,结果为’延迟两秒后的结果’。

3.3 Future.error

Future.error用于创建一个已经完成但包含错误的Future。

Future<String> createErrorFuture() { return Future.error(Exception(\'这是一个自定义异常\'));}

这里,createErrorFuture函数返回一个Future,但这个Future完成时会抛出一个Exception异常。

4. 处理 Future 结果

4.1 then 方法

then方法用于在Future完成后执行回调函数。它接收一个参数,即Future的结果。

void main() { Future<String> future = Future.value(\'Hello, Future!\'); future.then((result) { print(result); // 输出: Hello, Future! });}

在这个示例中,future是一个已经完成的Future,其结果为’Hello, Future!\'。通过调用then方法,我们可以在Future完成后处理这个结果。

4.2 catchError 方法

catchError用于捕获Future执行过程中抛出的错误。它可以与then链式调用。

void main() { Future<String> errorFuture = Future.error(Exception(\'模拟错误\')); errorFuture .then((result) { print(result); // 不会执行,因为Future包含错误 }) .catchError((error) { print(\'捕获到错误: $error\'); // 输出: 捕获到错误: Exception: 模拟错误 });}

在上述代码中,errorFuture是一个包含错误的Future。then方法中的回调函数不会执行,因为Future在完成时抛出了错误。而catchError方法捕获到了这个错误,并打印出错误信息。

4.3 whenComplete 方法

whenComplete在Future完成(无论成功或失败)时执行回调。它不接收Future的结果或错误。

void main() { Future<String> successFuture = Future.value(\'成功结果\'); successFuture .then((result) { print(result); // 输出: 成功结果 }) .catchError((error) { print(\'捕获到错误: $error\'); // 不会执行,因为Future成功 }) .whenComplete(() { print(\'Future已完成,无论成功或失败\'); // 输出: Future已完成,无论成功或失败 });}

在这个例子中,successFuture是一个成功的Future。then方法处理成功结果,catchError方法不会执行,最后whenComplete方法的回调函数在Future完成后执行。

5. Future 链式调用

由于then方法返回的仍然是一个Future,因此可以进行链式调用,以处理一系列有依赖关系的异步操作。

假设有如下三个异步任务分别为登录、获取用户信息、和保存用户信息。我们的代码要实现的功能是登录成功后获取用户信息然后保存用户信息到本地。这三个任务是按顺序且有依赖关系的,获取用户信息任务依赖登录接口返回的用户 id,保存用户信息任务依赖获取用户信息任务返回的结果。

Future<int> login(String username, String password) async { // 模拟登录操作,返回用户id await Future.delayed(Duration(seconds: 1)); return 12345;}Future<String> getUserInfo(int userId) async { // 模拟获取用户信息操作,返回用户信息 await Future.delayed(Duration(seconds: 1)); return \'用户信息: id=$userId\';}Future<void> saveUserInfo(String userInfo) async { // 模拟保存用户信息操作 await Future.delayed(Duration(seconds: 1)); print(\'保存用户信息: $userInfo\');}void main() { login(\'张三\', \'123456\') .then((id) { print(\'登录成功,用户id为${id}\'); return getUserInfo(id); }) .then((userInfo) { print(\'获取用户信息成功,结果为${userInfo}\'); return saveUserInfo(userInfo); }) .then((_) { print(\'保存用户信息成功\'); }) .catchError((error) { print(\'发生错误: $error\'); });}

在上述代码中,login函数返回一个Future,代表登录操作的结果(用户 id)。在第一个then回调中,使用返回的用户 id 调用getUserInfo函数,getUserInfo函数返回一个Future,代表用户信息。接着在第二个then回调中,使用获取到的用户信息调用saveUserInfo函数,saveUserInfo函数返回一个Future。通过链式调用,我们清晰地表达了这一系列异步操作的顺序和依赖关系。同时,catchError方法用于捕获整个过程中可能出现的任何错误。

6. 高级用法

6.1 Future.wait

Future.wait用于并行执行多个Future,并在所有Future都完成后返回一个包含所有结果的Future。假设我们有两个独立的异步任务,获取用户信息和获取用户订单信息,并且这两个任务没有依赖关系,我们希望同时执行这两个任务并在它们都完成后处理结果。

Future<String> getUserInfo() async { await Future.delayed(Duration(seconds: 2)); return \'用户信息\';}Future<String> getUserOrders() async { await Future.delayed(Duration(seconds: 1)); return \'用户订单信息\';}void main() { Future.wait([getUserInfo(), getUserOrders()]).then((results) { print(\'用户信息: ${results[0]}\'); print(\'用户订单信息: ${results[1]}\'); }).catchError((error) { print(\'发生错误: $error\'); });}

在这个例子中,Future.wait接收一个Future列表,同时执行getUserInfo和getUserOrders这两个异步任务。当这两个任务都完成后,then回调中的results参数将包含这两个任务的结果。这样可以显著提高效率,因为两个任务是并行执行的,而不是顺序执行。

6.2 Future.any

Future.any用于执行多个Future,只要其中一个Future完成,就返回该Future的结果。

Future<String> task1() async { await Future.delayed(Duration(seconds: 3)); return \'任务1完成\';}Future<String> task2() async { await Future.delayed(Duration(seconds: 1)); return \'任务2完成\';}void main() { Future.any([task1(), task2()]).then((result) { print(result); // 输出: 任务2完成 }).catchError((error) { print(\'发生错误: $error\'); });}

在上述代码中,task1和task2是两个异步任务,Future.any会同时启动这两个任务。由于task2执行时间更短,当task2完成时,Future.any就会返回task2的结果,而不会等待task1完成。

6.3 Completer

Completer可以用来创建一个可手动控制完成状态的Future。这在一些需要更灵活地控制异步操作完成时机的场景中非常有用。

void main() { Completer<String> completer = Completer(); Future<String> future = completer.future; future.then((result) { print(result); // 输出: 手动完成的结果 }); // 模拟在某个异步操作完成后手动完成Future Future.delayed(Duration(seconds: 2)).then((_) { completer.complete(\'手动完成的结果\'); });}

在这个例子中,我们通过Completer创建了一个Future。一开始这个Future处于未完成状态,直到两秒后,通过调用completer.complete方法手动完成这个Future,并传递结果’手动完成的结果’,此时Future的then回调函数被触发。

7. 注意事项

7.1 错误处理

在异步编程中,务必正确处理错误。通过catchError方法捕获Future执行过程中的错误,避免程序因未处理的异常而崩溃。同时,在async函数中使用try-catch块来捕获可能抛出的异常也是一种良好的实践。

Future<String> fetchData() async { try { // 模拟可能失败的异步操作 await Future.delayed(Duration(seconds: 2)); throw Exception(\'模拟数据获取失败\'); } catch (e) { // 处理异常 return \'错误信息: $e\'; }}void main() { fetchData().then((result) { print(result); // 输出: 错误信息: Exception: 模拟数据获取失败 });}

在上述代码中,fetchData函数内部使用try-catch块捕获可能抛出的异常,并返回错误信息。在调用fetchData的then回调中,可以处理这个错误信息。

7.2 避免回调地狱

虽然Future的链式调用在一定程度上避免了回调地狱,但在处理多层嵌套的异步操作时,代码可能仍然变得难以阅读和维护。此时,可以使用async/await语法来简化代码结构,使异步代码看起来更像同步代码。

Future<int> login(String username, String password) async { await Future.delayed(Duration(seconds: 1)); return 12345;}Future<String> getUserInfo(int userId) async { await Future.delayed(Duration(seconds: 1)); return \'用户信息: id=$userId\';}Future<void> saveUserInfo(String userInfo) async { await Future.delayed(Duration(seconds: 1)); print(\'保存用户信息: $userInfo\');}void main() async { try { int id = await login(\'张三\', \'123456\'); String userInfo = await getUserInfo(id); await saveUserInfo(userInfo); print(\'保存用户信息成功\'); } catch (error) { print(\'发生错误: $error\'); }}

在这个使用async/await的例子中,代码结构更加清晰,易于理解和维护。await关键字会暂停当前异步函数的执行,直到等待的Future完成,然后返回Future的结果。需要注意的是,await只能在标记为async的函数内部使用。

7.3 事件循环与微任务队列

Dart 的事件循环负责管理事件队列和微任务队列。事件队列包含 I/O 操作、计时器等任务,而微任务队列包含高优先级的任务,通常用于Future的回调。事件循环会优先处理微任务队列,然后处理事件队列中的任务。这意味着,如果在Future的then回调中创建了新的微任务,这些微任务会在事件队列中的任务之前执行。理解这一点对于优化异步代码的执行顺序和性能非常重要。

void main() { print(\'开始\'); Future.delayed(Duration.zero).then((_) { print(\'Future回调\'); scheduleMicrotask(() { print(\'微任务\'); }); }); print(\'结束\'); // 输出顺序: 开始 -> 结束 -> Future回调 -> 微任务}

在上述代码中,Future.delayed(Duration.zero)创建了一个立即执行的Future,其then回调会在当前事件循环的末尾执行。在then回调中,通过scheduleMicrotask创建了一个微任务,这个微任务会在then回调执行完毕后,优先于事件队列中的其他任务执行。因此,最终的输出顺序是开始 -> 结束 -> Future回调 -> 微任务。

8. Future 与其他异步机制的对比

8.1 Future 与 Stream

Future用于处理单个异步操作的结果,而Stream则用于处理一系列异步操作的结果,就像一条持续流淌的数据流。例如,在处理文件读取时,如果是读取整个文件的内容,使用Future比较合适;而如果是读取一个不断产生数据的文件(如日志文件),则Stream更为适合。

// 使用Future读取整个文件Future<String> readFileContent(String path) async { // 模拟读取文件内容 await Future.delayed(Duration(seconds: 2)); return \'文件内容...\';}// 使用Stream读取文件的多行数据Stream<String> readFileLines(String path) async* { // 模拟逐行读取文件 for (int i = 1; i <= 3; i++) { await Future.delayed(Duration(seconds: 1)); yield \'第$i行内容\'; }}void main() { // 处理Future结果 readFileContent(\'test.txt\').then((content) { print(\'文件内容:$content\'); }); // 处理Stream结果 readFileLines(\'test.txt\').listen((line) { print(\'读取到一行:$line\'); });}

在上述代码中,readFileContent返回Future,用于获取整个文件的内容;readFileLines返回Stream,通过yield关键字逐行返回文件内容,listen方法用于监听Stream产生的数据。

8.2 Future 与 async/await

async/await是 Dart 中用于简化Future操作的语法糖,它让异步代码的编写和阅读更接近同步代码的风格,但async/await的底层仍然是基于Future实现的。

使用async标记的函数会返回一个Future,而await关键字用于等待Future的完成。在前面的内容中已经有相关示例,这里不再赘述。总体来说,async/await使代码结构更清晰,减少了回调函数的嵌套,是处理Future的推荐方式。

9. Future 的实际应用场景案例

9.1 网络请求

在移动应用和 Web 应用中,网络请求是非常常见的异步操作,Future被广泛用于处理网络请求的结果。

import \'dart:convert\';import \'package:http/http.dart\' as http;Future<Map<String, dynamic>> fetchUserInfo(String userId) async { final response = await http.get(Uri.parse(\'https://api.example.com/users/$userId\')); if (response.statusCode == 200) { return json.decode(response.body); } else { throw Exception(\'获取用户信息失败,状态码:${response.statusCode}\'); }}void main() async { try { Map<String, dynamic> userInfo = await fetchUserInfo(\'123\'); print(\'用户名:${userInfo[\'name\']}\'); print(\'年龄:${userInfo[\'age\']}\'); } catch (error) { print(\'发生错误:$error\'); }}

在这个例子中,fetchUserInfo函数使用http库发送网络请求,通过await等待请求完成,然后根据响应状态码处理结果。如果请求成功,返回解析后的用户信息;如果失败,抛出异常。在main函数中,使用try-catch块捕获可能的异常,处理用户信息。

9.2 文件操作

文件的读取和写入也是典型的异步操作,使用Future可以避免这些操作阻塞主线程。

import \'dart:io\';Future<String> readFile(String path) async { File file = File(path); if (await file.exists()) { return await file.readAsString(); } else { throw Exception(\'文件不存在:$path\'); }}Future<void> writeFile(String path, String content) async { File file = File(path); await file.writeAsString(content);}void main() async { try { // 写入文件 await writeFile(\'test.txt\', \'Hello, File!\'); print(\'文件写入成功\'); // 读取文件 String content = await readFile(\'test.txt\'); print(\'文件内容:$content\'); } catch (error) { print(\'文件操作错误:$error\'); }}

上述代码中,readFile和writeFile函数分别用于读取和写入文件,它们都返回Future。在main函数中,通过await依次执行文件写入和读取操作,并处理可能出现的异常。

10. Future 的性能优化

10.1 合理使用 Future.wait 提高并行效率

在有多个独立的异步操作时,使用Future.wait让它们并行执行,而不是顺序执行,可以显著提高效率。例如,同时获取用户的基本信息、订单信息和收藏列表,这三个操作没有依赖关系,可以并行处理。

Future<Map<String, dynamic>> fetchUserBasicInfo(String userId) async { await Future.delayed(Duration(seconds: 2)); return {\'id\': userId, \'name\': \'张三\'};}Future<List<dynamic>> fetchUserOrders(String userId) async { await Future.delayed(Duration(seconds: 1)); return [{\'orderId\': \'1001\', \'product\': \'商品1\'}, {\'orderId\': \'1002\', \'product\': \'商品2\'}];}Future<List<dynamic>> fetchUserFavorites(String userId) async { await Future.delayed(Duration(seconds: 1.5)); return [{\'favId\': \'2001\', \'title\': \'收藏1\'}, {\'favId\': \'2002\', \'title\': \'收藏2\'}];}void main() async { String userId = \'123\'; Stopwatch stopwatch = Stopwatch()..start(); // 并行执行三个异步操作 List<dynamic> results = await Future.wait([ fetchUserBasicInfo(userId), fetchUserOrders(userId), fetchUserFavorites(userId), ]); stopwatch.stop(); print(\'总耗时:${stopwatch.elapsedSeconds}秒\'); print(\'用户基本信息:${results[0]}\'); print(\'用户订单:${results[1]}\'); print(\'用户收藏:${results[2]}\');}

在这个例子中,三个异步操作的执行时间分别为 2 秒、1 秒和 1.5 秒,使用Future.wait并行执行后,总耗时约为 2 秒(取决于最长的那个操作),而如果顺序执行,总耗时则为 2 + 1 + 1.5 = 4.5 秒,效率提升明显。

10.2 避免不必要的 Future 嵌套

过多的Future嵌套会导致代码可读性差,且可能影响性能。应尽量使用链式调用或async/await来扁平化代码结构。

// 不好的写法:过多的Future嵌套void badExample() { Future.value(1).then((a) { Future.value(a + 1).then((b) { Future.value(b + 1).then((c) { print(\'结果:$c\'); }); }); });}// 好的写法:使用链式调用void goodExample1() { Future.value(1) .then((a) => Future.value(a + 1)) .then((b) => Future.value(b + 1)) .then((c) => print(\'结果:$c\'));}// 好的写法:使用async/awaitvoid goodExample2() async { int a = await Future.value(1); int b = await Future.value(a + 1); int c = await Future.value(b + 1); print(\'结果:$c\');}void main() { badExample(); goodExample1(); goodExample2();}

上述代码中,badExample使用了多层Future嵌套,代码结构混乱;goodExample1使用链式调用,使代码更清晰;goodExample2使用async/await,代码结构最接近同步代码,可读性最好。

11. 总结

Future是 Dart 异步编程的核心概念,它用于表示一个异步操作的结果。通过创建Future、处理Future的结果(使用then、catchError、whenComplete等方法或async/await语法)、进行链式调用以及使用Future.wait、Future.any等高级用法,可以灵活地处理各种异步场景。

在实际开发中,要注意正确处理Future执行过程中的错误,避免回调地狱,合理利用async/await简化代码,并根据具体场景选择合适的异步机制(如Future与Stream的选择)。同时,了解Future的性能优化技巧,能让我们编写的异步代码更高效、更可靠。