async和await实际上是Dart异步编程用于简化异步API操作的两个关键字。它的作用就是能够将异步的代码使用同步的代码结构实现。相信学习过之前的Future和Stream的文章就知道对于最终返回的值或者是异常都是采用**异步回调方式。**然而async-await就是为了简化这些异步回调的方式,通过语法糖的简化,将原来异步回调方式写成简单的同步方式结构。需要注意的是: 使用await关键字必须配合async关键字一起使用才会起作用。本质上async-await是相当于都Future相关API接口的另一种封装,提供了一种更加简便的操作Future相关API的方法。
1. 为什么需要async-await
通过学习之前异步编程中的Future我们知道,Future一般使用 then
和 catchError
可以很好地处理数据回调和异常回调。这实际上还是一种基于异步回调的方式,如果异步操作依赖关系比较复杂需要编写回调代码比较繁杂,为了简化这些步骤 async-await
关键字通过同步代码结构来实现异步操作,从而使得代码更加简洁和具有可读性,此外在异常处理方式也会变得更加简单。
1.1 对比实现代码
- Future实现方式
void main() {
_loadUserFromSQL().then((userInfo) {
return _fetchSessionToken(userInfo);
}).then((token) {
return _fetchData(token);
}).then((data){
print('$data');
});
print('main is executed!');
}
class UserInfo {
final String userName;
final String pwd;
bool isValid;
UserInfo(this.userName, this.pwd);
}
//从本地SQL读取用户信息
Future<UserInfo> _loadUserFromSQL() {
return Future.delayed(
Duration(seconds: 2), () => UserInfo('gitchat', '123456'));
}
//获取用户token
Future<String> _fetchSessionToken(UserInfo userInfo) {
return Future.delayed(Duration(seconds: 2), '3424324sfdsfsdf24324234');
}
//请求数据
Future<String> _fetchData(String token) {
return Future.delayed(
Duration(seconds: 2),
() => token.isNotEmpty
? Future.value('this is data')
: Future.error('this is error'));
}
输出结果:
- async-await实现方式
void main() async {//注意:需要添加async,因为await必须在async方法内才有效
var userInfo = await _loadUserFromSQL();
var token = await _fetchSessionToken(userInfo);
var data = await _fetchData(token);
print('$data');
print('main is executed!');
}
class UserInfo {
final String userName;
final String pwd;
bool isValid;
UserInfo(this.userName, this.pwd);
}
//从本地SQL读取用户信息
Future<UserInfo> _loadUserFromSQL() {
return Future.delayed(
Duration(seconds: 2), () => UserInfo('gitchat', '123456'));
}
//获取用户token
Future<String> _fetchSessionToken(UserInfo userInfo) {
return Future.delayed(Duration(seconds: 2), () => '3424324sfdsfsdf24324234');
}
//请求数据
Future<String> _fetchData(String token) {
return Future.delayed(
Duration(seconds: 2),
() => token.isNotEmpty
? Future.value('this is data')
: Future.error('this is error'));
}
输出结果:
1.2 对比异常下处理
- Future的实现
void main() {
_loadUserFromSQL().then((userInfo) {
return _fetchSessionToken(userInfo);
}).then((token) {
return _fetchData(token);
}).catchError((e) {
print('fetch data is error: $e');
}).whenComplete(() => 'all is done');
print('main is executed!');
}
- async-await的实现
void main() async {
//注意:需要添加async,因为await必须在async方法内才有效
try {
var userInfo = await _loadUserFromSQL();
var token = await _fetchSessionToken(userInfo);
var data = await _fetchData(token);
print('$data');
} on Exception catch (e) {
print('this is error: $e');
} finally {
print('all is done');
}
print('main is executed!');
}
通过对比发现使用Future.then相比async-await调用链路更清晰,基于异步回调的方式调用相比比较清晰。而在代码实现以及同步结构分析async-await显得更加简单且处理异常也更加方面,更加符合同步代码的逻辑。
2. 什么是async-await
2.1 async-await基本介绍
async-await本质上是对Future API的简化形式,将异步回调代码写成同步代码结构形式。async关键字修饰的函数总是返回一个Future对象,所以async并不会阻塞当前线程,由前面的EventLoop和Future我们都知道Future的最终会加入EventQueue中,而EventQueue执行是当main函数执行完毕后,才会检查Microtask Queue和Event Queue并处理它们。await关键字意味着中断当前代码执行流程直到当前的async方法执行完毕,如果没有执行完毕下面的代码将处于等待的状态。但是需要遵循下面两个规则:
- 要定义异步函数,请在函数主体之前添加async关键字
- **await关键字只有在async关键字修饰的函数才会有效 **
void main() {
print("executedFuture return ${executedFuture() is Future}");//所以输出true
print('main is executed!');
}
executedFuture() async {//async函数默认返回的是Future对象
print('executedFuture start');
await Future.delayed(Duration(seconds: 1), () => print('future is finished'));//await等待Future执行完毕
print('Future is executed end');//executedFuture end 输出必须等待await的Future结束后才会执行
}
输出结果: 分析下输出结果,首先main函数同步执行executedFuture函数和print函数,所以马上就会同步输出 “executedFuture start”,但是由于executedFuture是一个async函数,await等待一个Future, 在executedFuture函数作用域内,所以并且在await后面执行,所以需要等待Future数据到了才会执行后面语句,但是此时的executedFuture执行完毕,马上就执行了main函数中的**“main is executed!”, main执行结束后,就会去检查MicroTask Queue是否存在需要执行的微任务,如果没有就会继续检查Event Queue是否存在需要处理的Event(其中Future也是一种Event), 所以检查到executedFuture函数中的Future,就会把它交给EventLoop处理,Future执行完毕后,输出“future is finished”, 最后执行输出“Future is executed end”。** ** 如果把上述例子修改一下:
void main() async {//main函数变成一个async函数
await executedFuture(); //executedFuture加入await关键字
print('main is executed!');
}
executedFuture() async {
print('executedFuture start');
await Future.delayed(Duration(seconds: 1), () => print('future is finished'));
print('Future is executed end');//executedFuture end 输出必须等待await的Future结束后才会执行
}
输出结果:
分析输出结果,可能很多人都很疑惑为什么**“main is executed”**会输出在最后,其实很容易理解,因为这时候main函数变成了一个async函数,所以必须等待 await executedFuture()
执行完毕后才会执行后面的 print('main is executed!')
. 如果executedFuture没有执行完毕那么整个main函数后面代码只能等待中,所以一般没有直接给整个main函数加async关键字,这样会使得整个main函数强行变成了同步执行。
2.2 结合EventLoop理解async-await
通过上面介绍我们知道async-await本质实际上还是Future,本质还是通过向Event Queue中添加Event,然后EventLoop来处理它。但是它们在代码结构形式上完全不一样它是怎么做到的呢?一起来看下。先给出一个例子:
- Future.then的理解
class DataWrapper {
final String data;
DataWrapper(this.data);
}
Future<DataWrapper> createData() {
return _loadDataFromDisk().then((id) {
return _requestNetworkData(id);
}).then((data) {
return DataWrapper(data);
});
}
Future<String> _loadDataFromDisk() {
return Future.delayed(Duration(seconds: 2), () => '1001');
}
Future<String> _requestNetworkData(String id) {
return Future.delayed(
Duration(seconds: 2), () => 'this is id:$id data from network');
}
其实通过Future的链式调用执行逻辑可以把 createData
代码按事件进行拆分成下面形式, 可以看到createData函数分为1、2、3:
首先第1块是当 createData
函数被调用后,就同步调用执行 _loadDataFromDisk
函数,它返回的是一个Future对象,然后就是等待数据的到达EventLoop, 这样 _loadDataFromDisk
函数就执行结束了。
然后,当_loadDataFromDisk
函数Future的数据到来时,会触发第2块中的 then
函数回调,拿到id后就立即执行了 _requestNetworkData
函数发出一个HTTP请求,并且先返回一个Future对象,然后就是等待HTTP数据到达EventLoop被处理即可,这样_requestNetworkData
函数就结束了。
最后,当 _requestNetworkData
函数Future的数据到来时, 第二个 then
函数就被回调触发了,然后就是创建 DataWrapper
返回最终数据对象即可。那么整个createData函数返回的Future最终就返回真实数据,如果外部调用者调用了这个函数,那么在它的 then
函数中就能拿到最终的DataWrapper
- async-await的理解
class DataWrapper {
final String data;
DataWrapper(this.data);
}
Future<DataWrapper> createData() async {//createData添加async关键字,表示这是一个异步函数
var id = await _loadDataFromDisk(); //await执行_loadDataFromDisk从磁盘中获取到id
var data = await _requestNetworkData(id); //通过传入_loadDataFromDisk的id执行_requestNetworkData返回data
return DataWrapper(data); //最后返回DataWrapper对象
}
Future<String> _loadDataFromDisk() {
return Future.delayed(Duration(seconds: 2), () => '1001');
}
Future<String> _requestNetworkData(String id) {
return Future.delayed(
Duration(seconds: 2), () => 'this is id:$id data from network');
}
其实通过async-await的类似同步调用执行顺序逻辑也可以把 createData
代码按事件进行拆分成下面形式,只不过之前是通过 then
函数来断开,这里通过await关键字来断开分析, 可以看到createData函数分为1、2、3:
首先,当createData函数开始执行就会触发第一个等待,此时createData就会将自己Future对象返回给调用函数,注意:这里遇到第1个await等待并调用_loadDataFromDisk函数的时候,createData函数就会把自己Future对象返回给调用函数, 此时的createData函数就已经执行完毕,可能大家比较疑惑没有显式看到返回了一个Future对象,这是因为async关键字语法糖帮你做了。有的人又会疑惑下面第3步不是返回了吗?请注意:下面返回的是 DataWrapper
对象不是一个 Future<DataWrapper>
,所以当执行createData函数的时候碰到第1个await等待,就会马上return一个Future对象,然后createData函数就执行完毕了。触发第一个等待并且就执行 _loadDataFromDisk
函数,它返回的是一个Future对象, 然后就会一直等待者数据 id
到来。
然后,执行第2块代码,当_loadDataFromDisk
函数,它返回的是一个Future对象中的 id
数据到来时,就会触发第2个await等待并且调用_requestNetworkData
函数,发出HTTP网络请求并且先返回一个Future,然后使用await等待这个HTTP Future的到来。
最后,_requestNetworkData
函数返回Future中的数据到来后,就能拿到HTTP的data数据,最后返回一个DataWrapper, 那么整个createData函数的Future就拿到最终的数据DataWrapper。
3. 如何使用async-await
3.1 基本使用
通过对比一般同步实现、异步Future.then的实现、异步async-await实现,可以更好地理解async-await用法,使用async-await实际上就是相当于把它当作同步代码结构形式来写即可。下面也是以上面例子为例
- 同步实现
假设_loadDataFromDisk和_requestNetworkData函数都是同步执行的,那么就能很容易写成它们执行代码:
DataWrapper createData() {
var id = _loadDataFromDisk();//同步执行直接return id
var data = _requestNetworkData(id);//同步执行直接return data
return DataWrapper(data);
}
- 异步Future.then实现
Future<DataWrapper> createData() {//由于是异步执行,所以注意返回的对象是一个Future
return _loadDataFromDisk().then((id) {//需要在异步回调then函数中拿到id
return _requestNetworkData(id);
}).then((data) {//需要在异步回调then函数中拿到data
return DataWrapper(data);//最后返回最终的DataWrapper
});
}
- 异步async-await实现
整体代码结构形式和同步代码结构形式一模一样只是加了async-await关键字以及最后返回的是一个Future对象
Future<DataWrapper> createData() async {//createData添加async关键字,表示这是一个异步函数
//注意: createData遇到第1个await _loadDataFromDisk,就会先返回一个Future<DataWrapper>对象,结束了createData函数。
var id = await _loadDataFromDisk(); //await执行_loadDataFromDisk从磁盘中获取到id
var data = await _requestNetworkData(id); //通过传入_loadDataFromDisk的id执行_requestNetworkData返回data
return DataWrapper(data); //最后返回DataWrapper对象
}
3.2 异常处理
- 同步实现
同步执行代码实现异常处理也只能是我们常用的try-catch-finally。
DataWrapper createData() {
try {
var id = _loadDataFromDisk();
var data = _requestNetworkData(id);
return DataWrapper(data);
} on Exception catch (e) {
print('this is error');
} finally {
print('executed done');
}
}
- 异步Future.then实现
Future.then实现异步异常的捕获一般是借助 catchError
来实现的。
Future<DataWrapper> createData() {
return _loadDataFromDisk().then((id) {
return _requestNetworkData(id);
}).then((data) {
return DataWrapper(data);
}).catchError((e) {//catchError捕获异常
print('this is error: $e');
}).whenComplete((){
print('executed is done');
});
}
- 异步async-await实现
async-await实现异步异常的捕获和同步实现一模一样。
Future<DataWrapper> createData() async {
try {
var id = await _loadDataFromDisk();
var data = await _requestNetworkData(id);
return DataWrapper(data);
} on Exception catch (e) {
print('this is error: $e');
} finally {
print('executed is done');
}
}
4. async-await使用场景
其实关于async-await的使用场景, 学完上面的内容基本上都可以总结分析出来。
- 大部分Future使用的场景都可以使用async-await来替代,也建议使用async-await,毕竟人家这俩关键字就是为了简化Future API的调用,而且能将异步代码写成同步代码形式。代码简洁度上也能得到提升。
- 其实通过上面例子也能明显发现,对于一些依赖关系比较明显的Future,建议还是使用Future,毕竟链式调用功能非常强大,可以一眼就能看到每个Future之间的前后依赖关系。因为通过async-await虽然简化了回调形式,但是在某种程度上降低了Future之间的依赖关系。
- 对于依赖关系不明显且彼此独立Future,可以使用async-await。
5. 熊喵先生的小总结
到这里有关Dart异步编程中的async-await使用到这里就结束了,实际上async-await就是一个语法糖的使用,本质还是对Future API的使用,它的好处和优势就是可以将异步执行写成同步代码结构形式,我们也对它的代码结构进行拆分分析,清晰地分析了其语法糖背后的原理。这样对使用async-await非常有帮助。
感谢关注,熊喵先生愿和你在技术路上一起成长!