Stream(流)和 Future都是Dart:async库的核心API,对异步提供了非常好的支持。
Dart类库有非常多的返回Future或者Stream对象的函数。 这些函数被称为异步函数:它们只会在设置好一些耗时操作之后返回,比如像 IO操作。而不是等到这个操作完成。async和await关键词支持了异步编程,允许您写出和同步代码很像的异步代码。
Future
Future与JavaScript中的Promise非常相似,表示一个异步操作的最终完成(或失败)及其结果值的表示。简单来说,它就是用于处理异步操作的,异步处理成功了就执行成功的操作,异步处理失败了就捕获错误或者停止后续操作。一个Future只会对应一个结果,要么成功,要么失败。
由于本身功能较多,这里我们只介绍其常用的API及特性。还有,请记住,Future 的所有API的返回值仍然是一个Future对象,所以可以很方便的进行链式调用。
Future.then
为了方便示例,在本例中我们使用Future.delayed 创建了一个延时任务(实际场景会是一个真正的耗时任务,比如一次网络请求),即2秒后返回结果字符串”hi world!”,然后我们在then中接收异步结果并打印结果,代码如下:
Future.delayed(new Duration(seconds: 2),(){return "hi world!";}).then((data){print(data);});
Future.catchError
如果异步任务发生错误,我们可以在catchError中捕获错误,我们将上面示例改为:
Future.delayed(new Duration(seconds: 2),(){//return "hi world!";throw AssertionError("Error");}).then((data){//执行成功会走到这里print("success");}).catchError((e){//执行失败会走到这里print(e);});
在本示例中,我们在异步任务中抛出了一个异常,then的回调函数将不会被执行,取而代之的是 catchError回调函数将被调用;但是,并不是只有 catchError回调才能捕获错误,then方法还有一个可选参数onError,我们也可以它来捕获异常:
Future.delayed(new Duration(seconds: 2), () {//return "hi world!";throw AssertionError("Error");}).then((data) {print("success");}, onError: (e) {print(e);});
Future.whenComplete
有些时候,我们会遇到无论异步任务执行成功或失败都需要做一些事的场景,比如在网络请求前弹出加载对话框,在请求结束后关闭对话框。这种场景,有两种方法,第一种是分别在then或catch中关闭一下对话框,第二种就是使用Future的whenComplete回调,我们将上面示例改一下:
Future.delayed(new Duration(seconds: 2),(){//return "hi world!";throw AssertionError("Error");}).then((data){//执行成功会走到这里print(data);}).catchError((e){//执行失败会走到这里print(e);}).whenComplete((){//无论成功或失败都会走到这里});
Future.wait
有些时候,我们需要等待多个异步任务都执行结束后才进行一些操作,比如我们有一个界面,需要先分别从两个网络接口获取数据,获取成功后,我们需要将两个接口数据进行特定的处理后再显示到UI界面上,应该怎么做?答案是Future.wait,它接受一个Future数组参数,只有数组中所有Future都执行成功后,才会触发then的成功回调,只要有一个Future执行失败,就会触发错误回调。下面,我们通过模拟Future.delayed 来模拟两个数据获取的异步任务,等两个异步任务都执行成功时,将两个异步任务的结果拼接打印出来,代码如下:
Future.wait([// 2秒后返回结果Future.delayed(new Duration(seconds: 2), () {return "hello";}),// 4秒后返回结果Future.delayed(new Duration(seconds: 4), () {return " world";})]).then((results){print(results[0]+results[1]);}).catchError((e){print(e);});
执行上面代码,4秒后你会在控制台中看到“hello world”。
Async/await
Dart中的async/await 和JavaScript中的async/await功能和用法是一模一样的,如果你已经了解JavaScript中的async/await的用法,可以直接跳过本节。
回调地狱(Callback Hell)
如果代码中有大量异步逻辑,并且出现大量异步任务依赖其它异步任务的结果时,必然会出现Future.then回调中套回调情况。举个例子,比如现在有个需求场景是用户先登录,登录成功后会获得用户ID,然后通过用户ID,再去请求用户个人信息,获取到用户个人信息后,为了使用方便,我们需要将其缓存在本地文件系统,代码如下:
//先分别定义各个异步任务Future<String> login(String userName, String pwd){...//用户登录};Future<String> getUserInfo(String id){...//获取用户信息};Future saveUserInfo(String userInfo){...// 保存用户信息};
接下来,执行整个任务流:
login("alice","******").then((id){//登录成功后通过,id获取用户信息getUserInfo(id).then((userInfo){//获取用户信息后保存saveUserInfo(userInfo).then((){//保存用户信息,接下来执行其它操作...});});})
可以感受一下,如果业务逻辑中有大量异步依赖的情况,将会出现上面这种在回调里面套回调的情况,过多的嵌套会导致的代码可读性下降以及出错率提高,并且非常难维护,这个问题被形象的称为回调地狱(Callback Hell)。回调地狱问题在之前JavaScript中非常突出,也是JavaScript被吐槽最多的点,但随着ECMAScript6和ECMAScript7标准发布后,这个问题得到了非常好的解决,而解决回调地狱的两大神器正是ECMAScript6引入了Promise,以及ECMAScript7中引入的async/await。 而在Dart中几乎是完全平移了JavaScript中的这两者:Future相当于Promise,而async/await连名字都没改。接下来我们看看通过Future和async/await如何消除上面示例中的嵌套问题。
使用Future消除Callback Hell
login("alice","******").then((id){return getUserInfo(id);}).then((userInfo){return saveUserInfo(userInfo);}).then((e){//执行接下来的操作}).catchError((e){//错误处理print(e);});
使用async/await消除callback hell
通过Future回调中再返回Future的方式虽然能避免层层嵌套,但是还是有一层回调,有没有一种方式能够让我们可以像写同步代码那样来执行异步任务而不使用回调的方式?答案是肯定的,这就要使用async/await了,下面我们先直接看代码,然后再解释,代码如下:
task() async {try{String id = await login("alice","******");String userInfo = await getUserInfo(id);await saveUserInfo(userInfo);//执行接下来的操作} catch(e){//错误处理print(e);}}
async用来表示函数是异步的,定义的函数会返回一个Future对象,可以使用then方法添加回调函数。await后面是一个Future,表示等待该异步任务完成,异步完成后才会往下走;await必须出现在async函数内部。
可以看到,我们通过async/await将一个异步流用同步的代码表示出来了。
其实,无论是在JavaScript还是Dart中,>
async/await> 都只是一个语法糖,编译器或解释器最终都会将其转化为一个Promise(Future)的调用链。
Stream
Stream 也是用于接收异步事件数据,和Future 不同的是,它可以接收多个异步操作的结果(成功或失败)。 也就是说,在执行异步任务时,可以通过多次触发成功或失败事件来传递结果数据或错误异常。 Stream 常用于会多次读取数据的异步任务场景,如网络内容下载、文件读写等。举个例子:
Stream.fromFutures([// 1秒后返回结果Future.delayed(new Duration(seconds: 1), () {return "hello 1";}),// 抛出一个异常Future.delayed(new Duration(seconds: 2),(){throw AssertionError("Error");}),// 3秒后返回结果Future.delayed(new Duration(seconds: 3), () {return "hello 3";})]).listen((data){print(data);}, onError: (e){print(e.message);},onDone: (){});
上面的代码依次会输出:
I/flutter (17666): hello 1I/flutter (17666): ErrorI/flutter (17666): hello 3
思考题:既然Stream可以接收多次事件,那能不能用Stream来实现一个订阅者模式的事件总线?
