本篇文章翻译自 👉Asynchronous programming: futures, async, await,算是在学习Dart异步时的资料了。
本篇文章会教给开发者如何使用 futures 和 async&await 关键字开发出异步代码。可以使用DartPad editors来运行测试案例。
为了最大限度的吸收本篇的内容,大家最好有以下方面的基础:
- 了解基本Dart语法 👉basic Dart syntax
- 有使用其他语言开发异步程序的经验
本篇文章涵盖了以下的内容:
- 何时以及如何使用
asyncandawait关键字 - 使用
asyncandawait关键字如何影响执行顺序的 - How to handle errors from an asynchronous call using
try-catchexpressions inasyncfunctions. - 如何在
async函数中使用try-catch表达式处理异步调用中的错误
学习本篇内容大概需要 40-60 分钟。
注意: 本片文章使用DartPads展示案例和练习。如果你在页面内没看到可以执行的DartPads,可以去DartPad troubleshooting page练习。
为什么异步代码很重要
异步的操作可以让你的程序,在等待一个操作完成的同时,继续执行另外一个操作。下面是一些常见的异步操作:
- 从网络获取数据
- 向数据库写数据
- 从文件读数据
在Dart中,可以使用 Future 类和 async&await 关键字来实现异步操作。
案例: 异步方法的不正确使用
Before running this example, try to spot the issue – what do you think the output will be?
下面的案例展示了异步方法 fetchUserOrder() 的错误使用,后面你可以使用 async&await 关键字来修复这个案例。在运行案例之前,可以先想一下,输出什么内容?
dart面板👉dartpad.cn/
代码如下:
// This example shows how *not* to write asynchronous Dart code.
String createOrderMessage() {
var order = fetchUserOrder();
return 'Your order is: $order';
}
Future<String> fetchUserOrder() =>
// Imagine that this function is more complex and slow.
Future.delayed(
const Duration(seconds: 2),
() => 'Large Latte',
);
void main() {
print(createOrderMessage());
}
案例中没有打印出 fetchUserOrder() 方法最终产生的值的原因是:
-
fetchUserOrder()是一个异步方法:在延迟一会儿之后,产生了一个 关于用户订单描述 “Large Latte” 的字符串。 -
为了获取用户的订单,
createOrderMessage()应该先调用fetchUserOrder()方法,并等待它完成。 由于createOrderMessage()没有等待fetchUserOrder()完成,所以createOrderMessage()就不能获得fetchUserOrder()的最终值也就是Future的值。 -
相反,
createOrderMessage()取得了一个即将完成的表示:未完成的Future。后面的章节我们会介绍Future。 -
Because
createOrderMessage()fails to get the value describing the user’s order, the example fails to print “Large Latte” to the console, and instead prints “Your order is: Instance of ‘_Future’”. -
由于
createOrderMessage()没有得到正确的描述用户订单的值,例子就没有在控制台打印出“Large Latte” 字符串。打印的字符串是 “Your order is: Instance of ‘_Future’”。
什么是future?
future是[Future]类的实例,它有未完成和完成两个状态。
注意: 未完成是Dart的一个术语,表示一个future还没有计算出值
未完成
当开发者调用一个异步方法的时候,它会返回一个未完成的future,future就会等待异步操作的完成或者抛出异常。
完成
如果异步操作成功了,future就会用一个值结束。否则就会跑出一个异常结束。
用一个值完成
Future<T> 实例会用T类型的一个值完成。比如 Future<String> 的future会计算出一个String的值。Future<void> 的future是不会计算出值的。
用一个异常结束
如果异步方法执行失败了,那么future就会用一个异常结束。
案例: 介绍future
在下面的例子中,fetchUserOrder() 返回一个future,这个future在打印到控制台之后完成。因为方法没有返回一个可用的值,所以 fetchUserOrder() 的返回类型是 Future<void>。在翻看结果之前,想一下是先打印 “Large Latte”还是“Fetching user order…”呢。
dart面板👉dartpad.cn/
代码如下:
Future<void> fetchUserOrder() {
// Imagine that this function is fetching user info from another service or database.
return Future.delayed(const Duration(seconds: 2), () => print('Large Latte'));
}
void main() {
fetchUserOrder();
print('Fetching user order...');
}
在上面的代码中,尽管 fetchUserOrder() 先于 main方法的 print() 执行,控制台依然先打印了'Fetching user order...',这是因为 fetchUserOrder()中延迟了“Large Latte”的打印。
案例: 用一个异常结束
下面的案例中,会用一个错误来完成future。稍后,会介绍如何处理这个错误。
dart面板👉dartpad.cn/
代码如下:
Future<void> fetchUserOrder() {
// Imagine that this function is fetching user info but encounters a bug
return Future.delayed(const Duration(seconds: 2),
() => throw Exception('Logout failed: user ID is invalid'));
}
void main() {
fetchUserOrder();
print('Fetching user order...');
}
在这个案例中, fetchUserOrder() 用一个用户Id不合法的错误来完成future。
现在已经介绍了future以及future的完成。但是还没涉及异步方法结果的使用。下面您将学习到如何使用 async and await 关键字来获取异步结果.
快速回顾:
- Future<T>的实例会计算出一个
T类型的值Future<void>的future不会计算出一个可用的值- 一个future要么是完成的状态,要么是未完成的状态。
- 当你调用一个返回future的方法时,方法会把计算加到队列中,并返回一个未完成的future。
- 当一个future的操作完成时,future会用一个值或者异常结束。
关键字:
- Future: Dart的类Future。
- future:
Future类的一个实例
处理 futures: async & await
async & await关键字可以很方便的定义一个异步方法,并使用方法的结果。当使用 async 和 await 关键字的时候可以记住两个基本的原则:
- 要定义一个异步方法的话,在方法体之前添加
async关键字 await关键字只能使用在async标注的方法里
这个案例是把 main() 从同步方法转为异步方法。
首先, 在方法体前面添加 async 关键字:
void main() async { ··· }
如果方法有一个声明的返回类型,然后将类型更新为 Future<返回类型> 。如果方法没有明确的返回类型,那么返回类型就是 Future<void>,main方法没有明确的返回类型:
Future<void> main() async { ··· }
现在就有了一个 async 方法,那么就可以在方法中使用 await 关键字了,使用 await 等待一个future完成:
print(await createOrderMessage());
The only differences are highlighted in the asynchronous example, which — if your window is wide enough — is to the right of the synchronous example.
下面的两字例子展示了:async 和 await 关键字可以让异步的代码看起来就像同步代码一样。不同点是在异步的案例中,我是用了(不同点)进行了标注。
案例: 同步方法
代码如下:
String createOrderMessage() {
var order = fetchUserOrder();
return 'Your order is: $order';
}
Future<String> fetchUserOrder() =>
// Imagine that this function is
// more complex and slow.
Future.delayed(
const Duration(seconds: 2),
() => 'Large Latte',
);
void main() {
print('Fetching user order...');
print(createOrderMessage());
}
结果如下:
Fetching user order...
Your order is: Instance of '_Future<String>'
案例: 异步方法
代码如下:
Future<String>(不同点)createOrderMessage() async (不同点){
var order = await(不同点) fetchUserOrder();
return 'Your order is: $order';
}
Future<String> fetchUserOrder() =>
// Imagine that this function is
// more complex and slow.
Future.delayed(
const Duration(seconds: 2),
() => 'Large Latte',
);
Future<void>(不同点) main() async (不同点){
print('Fetching user order...');
print(await(不同点) createOrderMessage());
}
结果如下:
Fetching user order...
Your order is: Large Latte
异步案例中的不同点在下面几点:
createOrderMessage()的返回类型从String变为Future<String>。createOrderMessage()和main()方法在方法体之前添加了async关键字- 在调用异步方法
fetchUserOrder()和createOrderMessage()之前添加了await关键字。
关键字:
- async: 在方法体之前使用
async关键字可以将方法标记为异步方法- async function:
async关键字标记的方法- await: 可以使用
await关键字获得异步表达式的结果,并且await关键字只能在asyncfunction方法内
async 和 await 的执行时许
async 方法在第一个 await 关键字之前会同步的执行。这就是说,在一个 async 方法内,第一个 await 关键字之前的同步代码会立即执行。
案例: 异步方法内的执行
Run the following example to see how execution proceeds within an async function body. What do you think the output will be?
运行下面的异步方法代码,观察一下异步方法内部是怎么运行的。先想一下下面👇的代码会输出什么。
dart面板👉dartpad.cn/
代码如下:
Future<void> printOrderMessage() async {
print('Awaiting user order...');//第一处
var order = await fetchUserOrder();//第二处
print('Your order is: $order');
}
Future<String> fetchUserOrder() {
// Imagine that this function is more complex and slow.
return Future.delayed(const Duration(seconds: 4), () => 'Large Latte');
}
Future<void> main() async {
countSeconds(4);
await printOrderMessage();
}
// You can ignore this function - it's here to visualize delay time in this example.
void countSeconds(int s) {
for (var i = 1; i <= s; i++) {
Future.delayed(Duration(seconds: i), () => print(i));
}
}
运行的结果如下:
Awaiting user order...
1
2
3
4
Your order is: Large Latte
你想对了吗?🤨
如果我们把上面代码的第一处和第二处进行对调,会怎么打印呢?
var order = await fetchUserOrder();
print('Awaiting user order...');
注意,输出的顺序发生了变化,现在'Awaiting user order...'出现在了数字之后。
练习 : 使用 async 和 await 实操
下面的练习代码是不正确的单元测试代码,包含了一些代码片段。您需要编写一些代码来完成练习,让不正确的代码正确。您不需要实现 main 方法
为了模拟异步操作,调用下面的方法:
| 方法 | 类型签名 | 方法描述 |
|---|---|---|
| fetchRole() | Future<String> fetchRole() | 用户角色的简单描述 |
| fetchLoginAmount() | Future<int> fetchLoginAmount() | 获取用户的登录次数 |
Part 1: reportUserRole()
添加代码到 reportUserRole() 方法,使方法执行下面的操作:
-
Returns a future that completes with the following string:
"User role: <user role>" -
返回一个future,future需要以"User role: "字符串结束。
- 注意: 必须使用
fetchRole()的返回值; 复制粘贴案例的返回值不会通过测试。 - 案例的返回值:
"User role: tester"
- 注意: 必须使用
-
通过调用已经提供的
fetchRole()方法获取用户的角色。
Part 2: reportLogins()
实现异步的 reportLogins() 方法,该方法需完成一下操作:
-
返回字符串:
"Total number of logins: <# of logins>".-
注意: 必须使用' fetchLoginAmount() '返回的真实值; 复制和粘贴示例返回值不会通过测试.
-
案例
reportLogins()的返回值:"Total number of logins: 57"
-
-
通过提供的 'fetchLoginAmount()' 函数获取登录次数。
代码片段如下:
// Part 1
// You can call the provided async function fetchRole()
// to return the user role.
Future<String> reportUserRole() async {
TODO('Your implementation goes here.');
}
// Part 2
// Implement reportLogins here
// You can call the provided async function fetchLoginAmount()
// to return the number of times that the user has logged in.
reportLogins(){}
调用关系如下:
Future<String> reportUserRole() async {
var username = await fetchRole();
return 'User role: $username';
}
Future<String> reportLogins() async {
var logins = await fetchLoginAmount();
return 'Total number of logins: $logins';
}
鉴于Markdown不能嵌入iframe,这个dartPad不好嵌入,麻烦大家这里运行啦👉dart面板👉dartpad.cn/
处理异常
使用try-catch来处理 async 方法中的异常
try {
print('Awaiting user order...');
var order = await fetchUserOrder();
} catch (err) {
print('Caught error: $err');
}
在 async 方法内部,可以像同步方法一样写 try-catch 片段 。
案例: async & await 和 try-catch
运行下面的代码,观察在异步方法内部如何处理异常。不过可以先想一下会输出什么。
dart面板👉dartpad.cn/
代码如下:
Future<void> printOrderMessage() async {
try {
print('Awaiting user order...');
var order = await fetchUserOrder();
print(order);
} catch (err) {
print('Caught error: $err');
}
}
Future<String> fetchUserOrder() {
// Imagine that this function is more complex.
var str = Future.delayed(
const Duration(seconds: 4),
() => throw 'Cannot locate user order');
return str;
}
Future<void> main() async {
await printOrderMessage();
}
练习: 实操处理异常
下面的练习就是使用上面的方法在异步方法内部处理异常,为了模拟异步操作,您的代码会调用下面的方法:
| Function | 类型签名 | 方法描述 |
|---|---|---|
| fetchNewUsername() | Future<String> fetchNewUsername() | 返回可以替代旧用户名的新用户名 |
使用 async 和 await to 实现异步的 changeUsername() 方法,该方法完成下面的操作:
-
调用已经提供的
fetchNewUsername()异步方法 并且 返回方法的结果- 案例返回
changeUsername()的结果:"jane_smith_92"
- 案例返回
-
Catche 出现的任何异常并异常的String值。
-
可以使用toString()方法来获取 Exceptions 和 Errors 的字符串。
代码片段如下:
// Implement changeUsername here
changeUsername() {}
调用关系如下:
Future<String> changeUsername () async {
try {
return await fetchNewUsername();
} catch (err) {
return err.toString();
}
}
鉴于Markdown不能嵌入iframe,这个dartPad不好嵌入,麻烦大家这里运行啦👉dart面板👉dartpad.cn/
练习: 把这些都结合起来
可以在一个练习中实操所有已经学到的东西啦,为了模拟异步操作,练习提供了 fetchUsername() 和 logoutUser() 异步方法:
| 方法 | 类型签名 | 方法描述 |
|---|---|---|
| fetchUsername() | Future<String> fetchUsername() | 返回当前的用户名 |
| logoutUser() | Future<String> logoutUser() | 登出当前的用户并返回用户名 |
开发以下内容:
第一部分: addHello()
- 开发一个单一的String类型的入参
addHello()方法 addHello()返回‘Hello ‘拼接入参的String。
案例:addHello('Jon')返回'Hello Jon'.
第二部分: greetUser()
- 开发一个无参的
greetUser()方法 greetUser()调用之前提供的的fetchUsername()异步方法获取用户名greetUser()调用addHello()来问候用户,传递用户名给addHello()然后返回执行的结果。
案例: 如果fetchUsername()返回'Jenny', 那么greetUser()凡娜'Hello Jenny'.
第三部分: sayGoodbye()
-
开发一个
sayGoodbye(),完成下面的操作:- 无入参.
- Catche所有的异常.
- 调用之前提供的
logoutUser()异步方法
-
如果
logoutUser()失败了,sayGoodbye()可以返回任何字符串 -
如果
logoutUser()成功了,sayGoodbye()返回字符串'<result> Thanks, see you next time',<result>logoutUser()方法的结果
代码片段如下:
// Part 1
addHello(user){}
// Part 2
// You can call the provided async function fetchUsername()
// to return the username.
greetUser(){}
// Part 3
// You can call the provided async function logoutUser()
// to log out the user.
sayGoodbye(){}
调用关系如下:
String addHello(user) => 'Hello $user';
Future<String> greetUser() async {
var username = await fetchUsername();
return addHello(username);
}
Future<String> sayGoodbye() async {
try {
var result = await logoutUser();
return '$result Thanks, see you next time';
} catch (e) {
return 'Failed to logout user: $e';
}
}
鉴于Markdown不能嵌入iframe,这个dartPad不好嵌入,麻烦大家这里运行啦👉dart面板👉dartpad.cn/
接下来是什么?
感谢🙏, 目前已经完成了本章的学习! 如果您想要学的更多,下面👇是一些建议:
-
尝试DartPad.
-
使用codelab.
-
学习更多的关于 futures 和 异步的东西:
- Streams tutorial: 学习一系列的异步事件是如何运作的。
- Dart videos from Google: 观看更多的异步开发视频, 或者 看一篇文章isolates and event loops.,这篇文章更多的是原理。
If you’re interested in using embedded DartPads, like this codelab does, see best practices for using DartPad in tutorials.
如果对使用DartPads感兴趣,可以看在课程中使用 DartPad 的最佳实践 tutorials这篇文章。