Flutter异步编程-async和await

1,957 阅读8分钟

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'));
}

输出结果: image.png

  • 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'));
}

输出结果: image.png

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结束后才会执行
}

输出结果: image.png 分析下输出结果,首先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结束后才会执行
}

输出结果: image.png 分析输出结果,可能很多人都很疑惑为什么**“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: image.png 首先第1块是当 createData 函数被调用后,就同步调用执行 _loadDataFromDisk 函数,它返回的是一个Future对象,然后就是等待数据的到达EventLoop, 这样 _loadDataFromDisk 函数就执行结束了。 image.png 然后,当_loadDataFromDisk 函数Future的数据到来时,会触发第2块中的 then 函数回调,拿到id后就立即执行了 _requestNetworkData 函数发出一个HTTP请求,并且先返回一个Future对象,然后就是等待HTTP数据到达EventLoop被处理即可,这样_requestNetworkData 函数就结束了。 image.png 最后,当 _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: image.png 首先,当createData函数开始执行就会触发第一个等待,此时createData就会将自己Future对象返回给调用函数,注意:这里遇到第1个await等待并调用_loadDataFromDisk函数的时候,createData函数就会把自己Future对象返回给调用函数, 此时的createData函数就已经执行完毕,可能大家比较疑惑没有显式看到返回了一个Future对象,这是因为async关键字语法糖帮你做了。有的人又会疑惑下面第3步不是返回了吗?请注意:下面返回的是 DataWrapper 对象不是一个 Future<DataWrapper> ,所以当执行createData函数的时候碰到第1个await等待,就会马上return一个Future对象,然后createData函数就执行完毕了。触发第一个等待并且就执行 _loadDataFromDisk 函数,它返回的是一个Future对象, 然后就会一直等待者数据 id 到来。 image.png 然后,执行第2块代码,当_loadDataFromDisk 函数,它返回的是一个Future对象中的 id 数据到来时,就会触发第2个await等待并且调用_requestNetworkData  函数,发出HTTP网络请求并且先返回一个Future,然后使用await等待这个HTTP Future的到来。 image.png 最后,_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非常有帮助。

感谢关注,熊喵先生愿和你在技术路上一起成长!