【Flutter基础】Dart中的异步 Future
前言
上一篇文章《【Flutter基础】Dart中的并发Isolate》中,我们介绍了 Isolate 的相关知识点。今天我们就回过头来看一看 Future,Future 可以说是我们在项目中使用最多的异步调用对象,但我们真的完全了解 Future 吗?看完这篇文章,我想你能够找到答案~
项目源码地址 Github
一、Future的基本用法
1.1 Future(FutureOr computation())
这里有一个计数器的例子,通过 Future(FutureOr<T> computation())
来实现异步计数功能,代码如下:
/// 1.1 Future(FutureOr<T> computation())
void futureTest() {
int _count = 0;
print('1.开始count=$_count');
Future(() {
for (int i = 0; i < 10000000; i++) {
_count++;
}
print('2.计算完成count=$_count');
});
print('3.结束count=$_count');
}
运行结果如下:
可以看到,Future 中的执行函数,会在最后执行打印,且不会阻塞后续的代码执行。从而导致打印结果为:1-3-2,而不是 1-2-3。
1.2 Future.value()
Future.value()
方法非常简单,直接返回一个 Future 的返回值:
/// 1.2 Future.value
void futureValueTest() {
Future<int>.value(2021).then((value) {
print('value: $value');
});
}
运行结果:
1.3 Future.then
为了解决上述打印顺序的问题,这里我们使用 Future.then
方法来实现代码的顺序加载。代码如下:
/// 1.3 Future.then
void futureThenTest() {
int _count = 0;
print('1.开始count=$_count');
Future(() {
for (int i = 0; i < 10000000; i++) {
_count++;
}
print('2.计算完成count=$_count');
}).then((value) => print('3.结束count=$_count'));
}
执行结果:
可以看到此时的打印顺序为 1-2-3 并且 3 获取到的值确实是计算后的值,这样就实现了异步调用的同步操作。
1.4 Future.delayed
通过 Future.delayed
我们能够实现延时调用功能,代码如下:
/// 1.4 Future.delayed
void futureDelayedTest() {
print('1.开始执行: ${DateTime.now()}');
Future.delayed(const Duration(seconds: 2), () {
print('2.延时2秒执行: ${DateTime.now()}');
});
print('3.结束执行: ${DateTime.now()}');
}
我们通过 delayed
延迟2秒执行计算,运行结果:
可以看到确实延迟了 2秒 执行计算任务。
1.5 await-async
除了使用 Future.then
方法来实现同步功能外,我们还可以使用 await-async
关键字来实现异步调用的同步功能。代码如下:
/// 1.5 async 和 await
Future<void> awaitAsyncTest() async {
int _count = 0;
print('1.开始count=$_count');
await Future(() {
for (int i = 0; i < 10000000; i++) {
_count++;
}
print('2.计算完成count=$_count');
});
print('3.结束count=$_count');
}
运行结果:
不知道大家发现了没有,对比 futureValueTest()
和 awaitAsyncTest()
方法的使用,我们仅仅添加了 await-async
关键字就实现了异步转同步的操作,无需使用函数回调等嵌套回调处理,这让我们写的代码更加清晰明确,除此之外在某些其他方面 await-async
也是更有优势。
Future.then
与 await-async
调用的区别在于,Future.then
无需给方法添加 async
关键字,缺点在于返回值会嵌套到 Future.then
中,导致方法获取返回值更加麻烦。这里推荐使用 await-async
更加直观。
二、Future的高级用法
2.1 异常处理
2.1.1 catchError
我们可以通过 catchError
来捕获函数调用中抛出的异常,这里我们通过 throw Exception()
手动抛出错误。代码如下:
/// 2.1.1 catchError
void catchErrorTest() {
int _count = 0;
print('1.开始count=$_count');
Future(() {
_count++;
throw Exception('计算错误');
print('2.计算完成count=$_count');
})
.then((value) => print('3.计算完成count=$_count'))
.catchError((error) => print('4.捕获异常 : $error'))
.then((value) => print('5.计算完成count=$_count'));
}
运行结果:
分析输出结果,我们可以发现以下几点结论:
throw Exception
异常抛出时,在捕获前,后面的代码都不会再执行,所以 2、3 处没有打印。- 异常捕获后,可以通过
catchError
获取异常 error 信息,执行异常处理。 - 异常捕获后,后续代码仍然可以执行。
2.1.2 onError
除了 catchError
可以捕获异常外,我们还可以通过 Future.then
的 onError
方法捕获异常。代码如下:
/// 2.1.2 onError
void onErrorTest() {
int _count = 0;
print('1.开始count=$_count');
Future(() {
_count++;
throw Exception('计算错误1');
}).then((value) {
print('2.计算完成count=$_count');
}, onError: (error) {
print('3.捕获异常 : $error');
});
}
运行结果:
可以看到,当抛出异常时,Future.then
的 onValue
函数不会执行,而是在 onError
函数中捕获异常。
2.1.3 catchError 与 onError 的差异
为了对比 catchError 与 onError的差异,我们这里有一个 onError 的案例:
/// 2.1.3 catchError 与 onError 的差异
void futureErrorTest1() {
int _count = 0;
print('1.开始count=$_count');
Future(() {
_count++;
throw Exception('计算错误1'); // tag1
}).then((value) {
}, onError: (error) {
print('2.捕获异常 : $error'); // tag2
// throw Exception('计算错误2');
throw error;
}).then((value) => null, onError: (error) {
print('3.捕获异常 : $error'); // tag3
});
}
我们在 tag1
处抛出一个异常,在 tag2
处理进行捕获并继续抛出该异常,最后在 tag3
处理继续捕获异常。
运行结果:
可以看到,onError
不仅可以捕获事件源抛出的异常,也可以捕获后续执行函数抛出的异常。
我们再来看一下 catchError
的调用,代码如下:
/// 2.1.3 catchError 与 onError
void futureErrorTest2() {
int _count = 0;
print('1.开始count=$_count');
Future(() {
_count++;
throw Exception('计算错误1');
}).catchError((error) {
print('2.捕获异常 : $error');
throw Exception('计算错误2');
// throw error;
}).catchError((error) {
print('4.捕获异常 : $error');
});
}
运行结果:
可以看到,虽然 catchError
也能够正常捕获到异常,但是最后会抛出一个 Unhandled exception 的异常
出去。分析抛出的异常原因,是由于 catchError
回调必须返回一个 Future's
类型导致的。但是我们的Future本身没有返回值,那么该如何处理呢?
处理方式:
-
建议使用
async-await
和try-catch
而不是使用.catchError
,这样可以避免这种混淆(我们上面介绍其他方面的优势在这里也可以体现)。 -
如果一定要使用
catchError
可以在事件源上添加一个then
函数回调,此时会将原来的返回值由Future<Never>
类型转换成Future<Null>
类型,从而可以为catchError
提供一个Future
类型的返回值,如此即可避免该问题发生。// 2.1.3 catchError 与 onError Future<void> futureErrorTest3() async { int _count = 0; print('1.开始count=$_count'); Future(() { _count++; throw Exception('计算错误1'); }).then((value) { }).catchError((error) { print('2.捕获异常 : $error'); }); }
运行结果:
2.1.4 Future.error
除了上述通过 throw Exception
抛出异常外,我们还是通过 Future.error
来抛出一个异常。代码如下:
/// 2.1.4 Future.error
futureErrorTest(){
int _count = 0;
print('1.开始count=$_count');
Future(() {
_count++;
return Future.error(Exception('计算错误1'));
}).catchError((error) {
print('2.捕获异常 : $error');
});
}
运行结果:
这里我们发现通过
Future.error
抛出的异常,并不会导致catchError
报错。因此对于catchError
的处理还有一种方式,那就是将Exception
通过Future.error
来转换。
2.2 Future.whenComplete
在 Future 中 Future.whenComplete
表示在整个 Future 链路执行完成后最终的调用。即使在 Future 链路调用中发生了异常,该方法也一定会执行。一般我们用来做一些 IO操作 的结束处理,比如:读写文件流的 close、数据库cursor 的关闭等。
/// 2.2 Future.whenComplete
void whenCompleteTest() {
Future(() {
throw Exception('计算错误1');
}).then((value){
}).catchError((error){
print('捕获异常: $error');
throw error;
}).whenComplete(() {
print('whenComplete');
});
}
运行结果:
2.3 Future.wait
不知道大家在工作中有没有遇到过这样的需求,某一个页面需要 2个接口的数据来拼接显示。此时,一般我们想到的是采用 await-async
来实现两个接口的数据同步处理,但这并不是最优解,因为两个接口的请求是串行的。我们希望的是两个接口能够同时发起请求,当两个接口请求都完成时即可进行回调。此时 Future.wait
就排上了用场。
Future.wait
表示同时执行多个异步任务,在所有任务执行完成,或者发生异常时进行回调。
// 2.3 Future.wait
void futureTest11() {
Future.wait([
Future(() => print('任务1')),
Future(() => print('任务2')),
Future(() => print('任务3')),
]).then((value) => print('完成所有任务'));
}
运行结果:
2.4 Future.timeout
Future 中 Future.timeout
方法表示给异步任务设置超时时长并在任务超时后进行回调处理。
/// 2.4 Future.timeout
void futureTest12() {
Future(() {
return Future.delayed(const Duration(seconds: 3), () => print('1.完成任务'));
}).timeout(const Duration(seconds: 2), onTimeout: () {
print('2.任务超时');
}).then((value) {
print('3.结束任务');
});
}
运行结果:
2.5 Future.doWhile
Future.doWhile
顾名思义在 Future中执行 do-while
处理。我们也可以直接在 Future 里面执行 do-while
循环,这里只不过封装了一层而已。代码如下:
/// 2.5 Future.doWhile
void doWhileTest() {
int _count = 0;
Future.doWhile(() async {
_count++;
await Future.delayed(const Duration(seconds: 1));
if (_count == 3) {
print('Finished with $_count');
return false;
}
return true;
});
}
运行结果:
2.6 Future.forEach
Future.forEach
也和我们日常用到的 forEach
一样,遍历每个item处理,遍历结束后,返回执行结果。
这里我们看一下 Future.forEach
源码,发现其内部是通过 Future.doWhile
来实现迭代器的遍历。
static Future<void> forEach<T>(
Iterable<T> elements, FutureOr action(T element)) {
var iterator = elements.iterator;
return doWhile(() {
if (!iterator.moveNext()) return false;
var result = action(iterator.current);
if (result is Future) return result.then(_kTrue);
return true;
});
}
2.7 Future.microtask
Future.microtask
表示将一个 Future 添加到微任务队列中执行,执行结束后回调到Future中。对于微任务队列大家可能比较陌生,更好奇为什么要这样处理,对于这些我们先按下不讲。后面介绍 Dart事件循环和队列 时我们在了解。源码:
factory Future.microtask(FutureOr<T> computation()) {
_Future<T> result = new _Future<T>();
scheduleMicrotask(() {
try {
result._complete(computation());
} catch (e, s) {
_completeWithErrorCallback(result, e, s);
}
});
return result;
}
2.8 Future.sync
Future.sync
顾名思义即Future同步的功能。下面是其实现源码:
factory Future.sync(FutureOr<T> computation()) {
var result = computation();
if (result is Future<T>) {
return result;
} else {
// TODO(40014): Remove cast when type promotion works.
return new _Future<T>.value(result as dynamic);
}
}
可以看到先执行 computation()
函数调用,若函数返回值也是 Future 则直接返回,否则通过 _Future.value() 进行包装。
看到这里大家可能有点懵逼,但 new _Future.value 和 Future.value
有啥区别?
这里有一个用例,能够让大家认知 Future.sync
与 Future.value
的区别。
/// 2.6 Future.sync
Future<void> syncTest() async {
Future future1 = Future.value(1); // tag1
Future future2 = Future<int>(() => 2); // tag5
Future future3 = Future.value(Future(() => 3)); // tag6
Future future4 = Future.sync(() => 4); // tag4
Future future5 = Future.sync(() => Future.value(5)); // tag2
scheduleMicrotask(() => print(6)); // tag3
future1.then(print);
future2.then(print);
future3.then(print);
future4.then(print);
future5.then(print);
}
输出结果:
分析上述结果:
Future.vaue
的优先级是最高的,无论Future.value
放在何处位置,都会优先执行完,因为它是同步的。Future.sync
对于computation
执行结果为Future<T>
的会直接返回,等价于执行Future.vaue(4)
,所以tag2
是第二个打印。tag3
处是将一个微任务添加到微任务队列中,在当前任务执行结束后,会优先进入到微任务队列的执行,所以tag3
处第三个打印。Future.aync
对于computation
结果为非 Future 类型的值,进行_Future<T>.value
包装。其优先级在事件队列之前、微任务队列之后,因此tag4
会打印。- 最后执行事件队列中的任务,
tag5
和tag6
会按添加顺序打印。
三、Dart事件循环和队列
3.1 Dart事件循环和队列
Dart 应用程序有一个带有两个队列的事件循环*——* 事件队列和微任务队列。
如下图所示,当main()执行时,事件循环开始工作。首先,它以 先进先出 顺序执行任何微任务。然后它出列并处理事件队列中的第一项。然后重复这个循环:执行所有微任务,然后处理事件队列中的下一个项目。
一般我们的代码都是在事件队列里面运行。这也是为什么代码执行出错后,程序缺没有崩溃,仍然可以执行其他代码的原因。
如何添加任务:
- Future类,将一个任务添加到事件队列的末尾。
- scheduleMicrotask()函数,将一个任务添加到微任务队列的末尾。
当我们使用Future()或
Future.delayed()时,即在事件队列末尾添加了一个新的任务。但也只是添加了一个任务,并不会立即执行,而是会等事件队列里的任务全部执行完成后,才会执行。这也是 [1.1小节](#1.1 Future(FutureOr computation()) 中执行结果不是按顺序1-2-3 打印的原因。
3.2 Future执行顺序
了解到了Dart的事件循环和队列机制,我们来测试下 Future 任务的执行顺序来帮助大家加深理解。
这里按照从上到下有1-9个编号打印,请大家思考下打印结果是怎样的顺序:
/// 3.2 event loop
void futureEventLoopTest() {
Future future1 = Future(() {
print('任务1');
});
future1.then((value) {
print('任务2');
scheduleMicrotask(() => print('任务3'));
}).then((value) {
print('任务4');
});
Future future2 = Future(() => print('任务5'));
Future(() => print('任务6'));
scheduleMicrotask(() => print('任务7'));
future2.then((value) => print('任务8'));
print('任务9');
}
我们来分析下上述的任务执行过程:
首先,我们可以分析出前面的代码一直是在事件队列和微任务队列添加任务,所以最后一行 任务9
的打印。
当前的任务已经执行完成,开始进入到微任务队列执行,而任务7
所在的任务是微任务,开始执行打印。
然后执行 future1,此时按顺序打印任务1
、任务2
、任务4
并把一个任务添加到微任务队列,执行结束。
然后,进入到微任务队列,执行任务3
的打印。
再进入到 future2 的执行,打印任务5
、任务8
,执行结束。
最后,一个 Future 执行,打印任务6
。
所以执行结果为:9-7-1-2-4-3-5-8-6
结语
至此,我们完成了对 Future 概念和用法的分析,不知道大家是否全部掌握呢?可以评论区留言。
如果觉得这篇文章对你有所帮助的话,不要忘记一键三连哦,大家的点赞是我更新的动力🥰。
项目源码:Github
参考资料:
转载自:https://juejin.cn/post/7238871553542438970