一、分析 Future 对象
对于 Dart
语言来说,异步使用的过程中,绝大多数场景和 Future
对象有关。C++
、Java
语言中也有 Future
的概念,对于 JavaScript/Typescript
来说就是 Promise
对象。它们是 异步任务结果
的封装,对 暂未完成
任务的一种 预期
(Future) 或 允诺
(Promise)。
1. 认识 Future 对象
前面说过,异步任务有三种状态。在任务分发之后,任务处于未完成
状态,而其返回值便是 Future
对象。比如上一篇中使用的文件异步写入方法 writeAsString
,其返回值是 Future<File>
类型。Future
可以指定一个泛型,该类型就是所 期待的结果
。
如下所示,在触发 writeAsString
方法之后,返回值是 一个
对未来的期待。这里期待返回的类型是 File
,也就是写入后的文件对象。
void saveToFile(TaskResult result) {
String filePath = path.join(Directory.current.path, "out.json");
File file = File(filePath);
String content = json.encode(result);
Future<File> futureFile = file.writeAsString(content);
}
Future
对象是在任务开始时生成的,认识这一点非常重要。而未来该任务会有什么结果,在 对象诞生时刻
是无法确定的。可能烧水会成功完成,获得 热水
结果;也可能烧水壶爆炸,任务失败,获得一个 异常
结果。
所以 ,对于 Future
对象而言,需要对其进行监听来 感知
任务回调的结果。Future
类中提供了进行监听的,两个非常重要的方法: then
和 catchError
分别监听 成功完成
和 异常结束
的场景。
2. Future 对任务回调的监听
如下 tag1
处,提供 Future#then
方法,可以监听到任务完成的时机,并且回调方法中的参数,就是期望的结果数据。
void saveToFile(TaskResult result) {
String filePath = path.join(Directory.current.path, "out.json");
File file = File(filePath);
String content = json.encode(result);
Future<File> futureFile = file.writeAsString(content);
futureFile.then((File file) { // tag1
print('写入成功:${file.path}');
});
}
下面来看异常情况,比如下面的 saveToErrorFile
方法,没有对应的文件夹,在写入文件时就会发生异常。通过 catchError
方法可以监听任务异常结束的时机,对异常结果进行处理。
void saveToErrorFile(TaskResult result) {
String filePath = path.join(Directory.current.path, "error","out.json");
File file = File(filePath);
String content = json.encode(result);
Future<File> futureFile = file.writeAsString(content);
// 监听任务成功完成
futureFile.then((File file) {
print('写入成功:${file.path}');
});
// 监听任务异常结束
futureFile.catchError((err) {
print("catchError:$err");
});
}
如果有些逻辑 无论任务成败
都需要执行,在 then
和 catchError
都有要进行书写,比较麻烦。这样的场景下,可以通过 whenComplete
来监听任务完成时机。如下日志可以看出,即使任务异常结束,也可以触发 whenComplete
回调逻辑。
futureFile.whenComplete((){
print("=======Complete=======");
});
3. 思考异步任务的异常抓取
由于这三个方法都会返回当前 Future 对象,在形式上可以连写。不知道你有没有发现这有点像啥?
futureFile.then((File file) {
print('写入成功:${file.path}');
}).catchError((err) {
print("catchError:$err");
}).whenComplete((){
print("=======Complete=======");
});
没错,这和代码的异常抓取非常像,whenComplete
用于无论如何都会执行的逻辑块,和 finally
代码块异曲同工;catchError
就是抓取异常信息,和 catch
代码块作用相同;then
用于任务执行成功的逻辑处理,和 try
代码块非常神似。
try...catch...finally
其实仔细想想,无论是同步还是异步,任何任务都有出错的可能。对于同步任务而言,比如类型转换异常、数值解析异常、分母为 0
异常等。如果预判当前逻辑中可能存在异常情况,需要通过 try...catch
来抓取处理。
同理,对于异步任务而言,本机体只是进行 任务分发
, 任务真正的执行过程在其他机体中。其他机体只能通过 回调
和本机体通信,所以对于异步任务而言自然需要回调来处理异常。
另外,对于异步任务而言,出现异常的可能性更大,因为其他机体的处理流程是不可控的。比如网络请求获取数据,需要 通过网络发送请求
、服务器处理请求
、服务器发送响应
,其中每个环节都可能出错,所以对异常的抓取是非常有必要的。
二、深入认识异步异常抓取
1. catchError 中 onError 的细节
在Future#catchError
方法源码注释中有相关说明:这里的第一参 onError
是 Function
类型,可以是红框中的两类函数:支持 一参
和 两参
。
如下日志所示,第二参是 _StackTrace
对象,可以根据它定位到出错代码的位置:
futureFile.catchError((err,stack) {
print("catchError::[${err.runtimeType}]::$err");
print("stack at ::[${stack.runtimeType}]::$stack");
});
2. 认识 FutureOr 对象
从上面可以看出 onError
方法需要返回一个 FutureOr
对象。如下,通不通过 then
处理,直接使用 futureFile
对象监听异常,如果不返回,会抛一个 ArgumentError
的异常,可谓 抓一送一
。
futureFile.catchError((err){
print("catchError::[${err.runtimeType}]::$err");
});
既然出异常,自然要解决,所以我们需要在异常回调方法中,返回一个 FutureOr
对象。
FutureOr
是一个比较特别的对象,在 Dart
源码中它只是一个私有构造的抽象类,它不可以被实例化。另外 vm:entry-point
表示它是和虚拟机打交道的。在日常开发中,我们只需要知道,该类型代表 Future<T>
和 T
类型。也就是说,它是 一类两型
的特殊存在。
@pragma("vm:entry-point")
abstract class FutureOr<T> {
// Private generative constructor, so that it is not subclassable, mixable, or
// instantiable.
FutureOr._() {
throw new UnsupportedError("FutureOr can't be instantiated");
}
}
比如 File
和 Future<File>
都可以作为 FutureOr
来看待,在 catchError
中可以返回一个 File
对象,或者 Future<File>
异步对象。
futureFile.catchError((err){
print("catchError::[${err.runtimeType}]::$err");
return File('this is error file');
});
3. catchError 方法的返回值
通过 catchError
方法的定义可以看出,它可以返回一个 Future<T>
对象。
Future<T> catchError(Function onError, {bool test(Object error)?});
如下,发生异常时,catchError
返回的 futureFile
对象,通过 then
监听时,会打印出 this is error file
这正是 onError
返回的文件对象。
futureFile = futureFile.catchError((err){
print("catchError::[${err.runtimeType}]::$err");
return File('this is error file');
});
futureFile.then((value){
print(value.path);
});
当没有发生异常时 catchError
返回值仍是原先任务对象:
4. catchError 第二参: test
另外 catchError
还有可选的第二参 test
,也是一个函数,返回一个 bool
值,入参是 error
对象。test
指定的函数会先于 onError
函数触发,返回值可以控制是否触发 onError
。
比如下面 test
中如果 error
不是 FileSystemException
才抓取,所以这时第一参 onError
就不会触发。简单来说 test
用于根据 error
信息,判断是否需要对异常进行抓取,一般来说很少使用,了解一下即可。
futureFile.catchError(
(err,stack){
print("catchError::[${err.runtimeType}]::$err");
print("stack at ::[${stack.runtimeType}]::$stack");
return File('this is error file');
},
test: (error)=> error is! FileSystemException
);
三、 异步成功回调的使用细节
1. then 方法的返回值
我们一般只是在 then
中监听异步任务完成的结果,从下面的 then
方法的定义可以看出:第一个入参是函数对象,其中函数参数是 T
类中值,也就是 Future
的泛型类型;该函数还有一个返回值,类型为 FutureOr<R>
。其中 R
泛型是方法指定的泛型,then
方法的返回值是 R
泛型的 Future
对象。
Future<R> then<R>(FutureOr<R> onValue(T value), {Function? onError});
也就是说,通过 then
方法,可以返回一个其他类型的 Future
对象。如下所示,为 then
方法泛型指定为 String
,这样在第一参函数中返回 FutureOr<String>
对象,then
的返回值就是 Future<String>
。
我们知道 File#readAsString
方法返回的是 Future<String>
,可以作为第一参的返回值,这样 then
会返回了一个异步任务 thenResult
。同样可以对这个异步任务使用 then
监听:
Future<String> thenResult = futureFile.then<String>((File file) {
print('写入成功:${file.path}');
return file.readAsString();
});
thenResult.then((String value){
print('读取成功:${value}');
});
上面拆开只是为了方便理解,如下可以通过连续的 then
调用。如果现在一个异步任务完成后,执行另一个异步任务,这种写法要方便一些。不过一般很少使用,了解一下即可:then
拥有返回另一异步任务的能力。
futureFile.then<String>((File file) {
print('写入成功:${file.path}');
return file.readAsString();
}).then((String value){
print('读取成功:${value}');
});
2. then 方法中的 onError
then
方法有两个入参,第一个是必传的回调函数,会将 T
类型的数据回调出来。除此之外,还有一个 onError
的可选回调,该错误回调参数也是异常和堆栈信息。
需要注意一点,如果在 then
中提供 onError
回调后,对 then
的返回值再监听 catchError
就会没有效果,如下所示:
futureFile.then((File file) {
print('写入成功:${file.path}');
},onError:(err,stack){
print("onError::[${err.runtimeType}]::$err");
print("onError stack at ::[${stack.runtimeType}]::$stack");
}).catchError((err){
print("catchError:$err");
});
3. whenComplete 方法
最后看一下 whenComplete
方法,该方法的回调中没有任何参数,更像一个 时机
的监听。表示任务结束时候需要执行的动作,和 finally
代码块是很类似的。
Future<T> whenComplete(FutureOr<void> action());
注意一下,whenComplete
方法会返回 Future<T>
对象,也就是该异步任务本身,所以你可以连续监听多个 whenComplete
事件。这样,一次异步任务完成后,三个成功事件的监听都会触发,感觉挺有意思的,虽然没太大卵用。
print("====start Task==${DateTime.now()}===");
Future future = Future.delayed(Duration(seconds: 3));
future.whenComplete((){
print("====whenComplete1==${DateTime.now()}===");
}).whenComplete((){
print("====whenComplete2==${DateTime.now()}===");
}).whenComplete((){
print("====whenComplete3==${DateTime.now()}===");
});
4、 async/await 关键字的使用
通过 then
进行回调,在写法上看起来比较臃肿,特别是当个多个异步任务需要按顺序执行时。比如,先读配置文件 config.json
;根据配置文件中的信息,读取一个资源文件 a.json
;在读取 a.json
成功之后,将文件中的 read_count
字段 +1
。
--->[config.json]---
{
"assets_file_path": "/Users/mac/Coder/Projects/juejin/async_task/config/a.json"
}
--->[a.json]---
{"ip":"198.164.88.001","port":"9090","read_count":11}
当三个异步任务需要依次执行,如果仅通过 then
方法来监听,就会导致回调中嵌套另一个异步任务的回调,让代码看起来很闹心。代码如下:
void readFile(TaskResult result) {
String filePath = path.join(Directory.current.path,"config","config.json");
File file = File(filePath);
Future<String> futureFile = file.readAsString();
futureFile.then((String value){
String assetsPath = json.decode(value)['assets_file_path'];
File file = File(assetsPath);
file.readAsString().then((String value){
print(value);
dynamic map = json.decode(value);
int count = map['read_count'];
map['read_count'] = count+1;
file.writeAsString(json.encode(map)).then((File file){
print("写入成功:${file.path}");
});
});
});
}
async/await
两个关键字的组合就是为了简化这种场景下的语法书写形式而存在的。语法规定, await
关键字只能在 async
修饰的方法中示意。如下所示,使用 await
关键字修饰 Future
对象后,返回值是结果类型。
代码中 tag1
处使用 await
修饰 Future
对象,表示必须等待做个异步对象完成后,才可以执行下一行代码。这样代码就可以用同步的方式书写,就像 冲水
需要等待 烧水
任务的结束,它们在逻辑上是同步的。 但这并不影响 扫地
和 烧水
在逻辑上是异步的。
void readFile(TaskResult result) async{
String filePath = path.join(Directory.current.path,"config","config.json");
File configFile = File(filePath);
String configContent = await configFile.readAsString(); // tag1
String assetsPath = json.decode(configContent)['assets_file_path'];
File assetsFile = File(assetsPath);
String assetsContent = await assetsFile.readAsString();
print(assetsContent);
dynamic map = json.decode(assetsContent);
int count = map['read_count'];
map['read_count'] = count+1;
File resultFile = await assetsFile.writeAsString(json.encode(map));
print("写入成功:${resultFile.path}");
}
await
的价值是简化异步任务完成监听,让依赖于任务结果的后续任务摆脱回调监听,从而以一种同步方式更简单地书写。对于某些任务,需要依赖异步任务结果的场景中,使用这两个关键字可以保证功能正确的条件下,让代码的可读性增加。其实这本质上就是个语法糖而已,认清到任务之间的关系,就很容易理解。
5、 async/await 使用的注意点
可能很多人(包括曾经的我)一看到异步方法,就下意识地选择 await
来获取结果,这是非常片面的。await
修饰的异步任务结束后才会继续向下执行,所以之后的逻辑和任务结果相关时,才有使用的价值。
如果两个异步任务没有关系的话,前一个任务使用 await
修饰,那么后一个任务只能等待前者结束才能分发。 比如 烧水
和 煮饭
两个异步任务没有什么关系,如果在处理 烧水
时使用 await
等待结果,将 煮饭
任务在下面代码中分发,这显然是对任务的不合理分配。
使用,在使用 async/await
处理时,要留个心眼:想一下其后的代码是否真的必须在该任务完成之后才处理;这个 await
的修饰,是否会阻塞到后面不相干的异步任务分发。而不是一味的使用 await
进行处理,这样在某些场景,任务分配不合理,就无法发挥出异步的最大功效。
四、结合应用场景介绍 Future 的使用
Future
作为 Dart
对单个异步任务的封装类,在使用上是非常方便的。掌握了上面的几点知识,能解决日常开发中 90%
对 Future 对象的使用场景。如下是 Future
类的结构图,其中有 6
个构造方法,4
个静态方法,5
个成员方法。一些不常用的功能,这里暂不介绍,在后期的文章中会做统一介绍。接下来,我们将结合 Flutter
应用开发,提供 Future
对象进一步地理解异步。
1. 场景应用介绍
现在将脱离 Dart
控制台打印,通过 Future
在 Flutter
应用中的表现对其深入了解。如下所示:在计数器初始项目基础上进行拓展,点击右下角按钮时,会执行一个异步任务。在异步任务执行的过程中,按钮显示 加载中
效果,且呈灰色不可点击。当任务执行完毕后恢复原样,每次异步任务完成之后,会让界面中的数字 +1
。
在开始,先定义一个 TaskState
的枚举,用于标识任务的状态,如下所示: initial
标识初始状态,loading
标识加载中,error
标识任务异常结束。
enum TaskState {
initial,
loading,
error,
}
这样,我们可以根据任务运行的状态 TaskState
,对按钮的构建逻辑通过方法进行封装。代码如下所示:逻辑很简单,就是不同的情况下,为 FloatingActionButton
组件提供不同的参数而已:现在重点就是看初始状态下 _doIncrementTask
方法如何执行异步任务。
Widget buildButtonByState(TaskState state) {
VoidCallback? onPressed;
Color color;
Widget child;
switch (state) {
case TaskState.initial:
child = const Icon(Icons.add);
onPressed = _doIncrementTask;
color = Theme.of(context).primaryColor;
break;
case TaskState.loading:
child = const CupertinoActivityIndicator(color: Colors.white);
color = Colors.grey;
onPressed = null;
break;
case TaskState.error:
child = const Icon(Icons.refresh);
color = Colors.red;
onPressed = renderLoaded;
break;
}
return FloatingActionButton(
backgroundColor: color,
onPressed: onPressed,
child: child,
);
}
2. Future.delayed 创建延时异步任务
Future.delayed
构造可以创建一个延迟的异步任务。由于第一入参可以指定任务消耗的时长,这经常被用于一些异步场景的模拟。其中 renderLoading
方法应用更新界面,显示 加载中
的效果,也就是将 _state
置为 TaskState.loading
再触发更新。renderLoaded
方法将界面置为初始状态。
从上面可以看出加载中的动画依然在运行中,所以异步的等待并不会阻塞界面线程。通过 await
关键字可以等待异步任务结束,再执行接下来的代码,使 _counter
自加,更新界面。从这三个任务,大家可以自己品味一下,异步的意义。
int _counter = 0;
TaskState _state = TaskState.initial;
void _doIncrementTask() async {
renderLoading();
// 模拟异步任务
await Future.delayed(const Duration(seconds: 2));
_counter++;
renderLoaded();
}
void renderLoading() {
setState(() {
_state = TaskState.loading;
});
}
void renderLoaded() {
setState(() {
_state = TaskState.initial;
});
}
3. 直观感受同步和异步任务的区别
可能有人还是体会不出,下面改一下代码,让你更直观的感受两者之间的差距。与 异步等待
相对应的是 同步等待
,使用 sleep
方法可以模拟同步等待的耗时任务。如下,将代码改为同步等待两秒,可以看出线程被阻塞,在此期间程序就无法做出任何反应,俗称 "卡死"
。
void _doIncrementTask() async {
renderLoading();
// 模拟同步等待任务耗时
sleep(const Duration(seconds: 2));
_counter++;
renderLoaded();
}
通过同步和异步等待的对比,想必你应该明白两者的差距。上面的 sleep
方法,在日常开发中就对应一下需要在 Dart
代码中处理的计算密集型的任务,比如下面的 loopAdd
方法,执行十亿次的累加计算,就是在同步执行一个耗时任务,卡住是必然的。当然也有解决的方案,在后期文章中会进行探讨。
void _doIncrementTask() async {
renderLoading();
loopAdd(1000000000);
_counter++;
renderLoaded();
}
int loopAdd(int count) {
int sum = 0;
for (int i = 0; i <= count; i++) {
sum+=i;
}
return sum;
}
3. Future.delayed 的第二参使用
很多人可能只知道 Future.delayed
只是作为异步延时一下,并没有在意它的第二参。如下所示,第二参数是提个函数,无回调参数,返回 FutureOr<T>
对象。也就是说 Future.delayed
也可以有异步任务的结果值。
Future.delayed(Duration duration, [FutureOr<T> computation()?])
Future.delayed
也可以模拟异步任务的返回结果、任务异常的情况。如下所示:当 counter
自加之后是 3
的倍数时,抛出异常。通过 computation
函数就可以实现:
代码如下:定义一个 computation
函数作为第二入参(函数名可任意指定),在其中对 counter % 3 == 0
时,通过 throw
抛出异常,来模拟异步任务的异常情况。如果没有异常,则返回 counter
值,该值将作为延时异步任务的结果值。
由于这里使用 await
关键字,之后的逻辑可以看作同步执行的,可以使用 try...catch
来抓取异常。
void _doIncrementTask() async {
renderLoading();
// 模拟异步任务
try {
_counter = await Future.delayed(const Duration(seconds: 1), computation);
renderLoaded();
} catch (e) {
renderError();
}
}
FutureOr<int> computation(){
int counter = _counter + 1;
if( counter % 3 == 0 ){
throw 'error';
}
return counter;
}
void renderError() {
setState(() {
_state = TaskState.error;
});
}
本文详细介绍了 Future
对象的使用,需要额外注意三个回调方法的使用方式,以及 async/await
关键字的使用场景。另外结合 Flutter
中的一个小应用案例,体会了一些异步在实际开发中的使用方式。不过 Future
仅是异步编程的一部分, 在某些场景下 Future
有其局限性,我们需要一种更高级的手段来处理,下一篇我们将进入流 Stream
的认知,敬请期待。那本就到这里,谢谢观看 ~