【Flutter 异步编程 -伍】 | 深入剖析 Future 类源码实现
一、Future 中的监听与通知
在日常开发中,我们一般知道对 Future
对象通过 then
、onError
设置回调方法进行监听。但很少有机会了解 Future
中的回调是何时触发的, Future
像一个黑箱一样,对它越是不了解,就越是畏惧。本文,将带大家从 Future
的源码出发,见识一下 Future
内部的风采。
1. 深入认识 Future.delayed
如下是 Future.delayed
构造方法的代码,可以看出 延迟异步任务
本质上是:通过 Timer
开启一个延迟回调。Future.delayed
构造中的第二入参是一个可空的 回调函数 - computation
,该函数触发的时机也很明显:如下 424 行
, 在 duration
时长之后,会触发 Timer
构造中传入的回调,当 computation
非空就会触发。
Duration delay = const Duration(seconds: 2);
Future.delayed(delay,(){
print("task1 done");
});
这里说明一个小细节:我们知道 Future
是一个抽象类,并不能直接实例化对象,但可以通过 factory
构造,创建 子类对象
返回。这里 Future.delayed
就是一个工厂构造,上图中 418
行会创建 _Future
对象 result
,在 430
行对 result
对象进行返回。
如下代码中 Future.delayed
方法没有第二参,说明构造时 computation
为空,走的是 421
行,触发 result
的 _complete
方法表示任务完成。这可以说明一个问题: Future#_complete
方法是触发 Future#then
回调的契机。
Future delayedTask = Future.delayed(delay);
delayedTask.then((value){
print("task2 done");
});
从 Future#_complete
方法中可以看出,其入参是 FutureOr<T>
,说明可以传入 Future
对象或 T
泛型对象。如果非 Future
对象,表示任务完成,会触发 _propagateToListeners
方法通知监听者们。
2. 探寻 Future 的回调监听
我们知道,Future
对象的完成时机,可以通过 then
方法中的回调进行监听。在运行时的实际类型是 _Future
,所以看一下它的 then
方法实现。如下所示,then
方法的第一参为回调函数,这里的函数名为 f
。
在 314
行会将 f
函数被注册到 currentZone
的回调中;如果 onError
非空,也会在 _registerErrorHandler
方法在,被注册到 currentZone
的回调在中。
最后会通过 _addListener
方法添加 _FutureListener
监听者对象,如下所示,可以推断出 _FutureListener
是一个链表结构,而且 _Future
类中持有链表的首节点 _resultOrListeners
。总而言之, _Future.then
中的主要工作是 注册回调
和 添加监听者
。
如下是 _FutureListener.then
的构造代码,可以看出 _FutureListener
中有 callback
和 errorCallback
两个函数对象,他们将在 then
构造中被赋值。结合 _Future#then
在创建 _FutureListener
的代码(323 行
)可知,用户传入的回调 f
作为第一参,也就是为 _FutureListener
的 callback
对象赋值。
也就是说,_FutureListener#callback
被调用的场合,就是 then
中成功结果的回调时机。就相当于 把鱼钩投入水中
(设置回调),接下来探索一下 何时鱼会上钩
(触发通知) 。
PS : 不知为何
Dart
中不允许对这块内容进行断点调试
和日志打印
,所以只能根据源码的功能、结合代码中的线索进行分析。如果有什么认知不对的地方,希望大家可以友善讨论。
3. 探寻 Future 的触发通知
从程序运行的逻辑上来看, Future#_complete
是触发 Future#then
中回调的原因。在 557
行所示,_propagateToListeners
方法在字面的意思上是通知监听者。下面来详细看一下:
在 551
行会先触发 _removeListeners
方法移除监听者,并返回 _FutureListener
对象。该对象将作为 _propagateToListeners
的第二入参,也就是需要被通知的监听者们。如下所示,_removeListeners
方法中会将 _Future
的 _resultOrListeners
成员置空,也就是移除监听者的表现。 另外,返回值是通过 _reverseListeners
方法返回的 _FutureListener
对象:
_reverseListeners
方法是一个非常经典的单链表反转操作:比如进入方法时,current
是链表的首节点 A
,且其后有 B
、C
节点;那么方法执行完后,链接结构就是 C
为首节点,其后是 B
、A
节点。最后一次 while
循环时,484
行 prev
被赋值为 current
,所以最终返回的 prev
对象就是首节点 C
。
从这里可以看出,_removeListeners
方法的作用是置空 _Future#_resultOrListeners
,并将监听者链表反序返回。
_propagateToListeners
是定义在 _Future
中的静态方法,从代码注释中能看出:它可以触发 listeners
的回调。其中有两个入参,其一是 _Future
对象,其二是 _FutureListener
对象。在该 _complete
中被调用时,第一参入参是 this
对象,第二入参是上面反序返回的监听者链表 首节点
。
_propagateToListeners
方法内定义了三个函数,handleWhenCompleteCallback
、handleValueCallback
、handleError
分别用于处理 完成
、正确结果
和 异常
。
其中 then
中的结果回调对应的是 handleValueCallback
:
如下,在 handleValueCallback
中,listener
会触发 handleValue
方法。其中 sourceResult
就是d当前 _Future
对象的 _resultOrListeners
。如下 tag1
处,在 _complete
方法 _setValue
时会将结果赋值给 _resultOrListeners
。
---->[_propagateToListeners]----
final dynamic sourceResult = source._resultOrListeners;
---->[_complete]----
void _complete(FutureOr<T> value) {
// 略...
_setValue(value as dynamic); // tag1
_propagateToListeners(this, listeners);
}
---->[_setValue]----
void _setValue(T value) {
assert(!_isComplete); // But may have a completion pending.
_state = _stateValue;
_resultOrListeners = value;
}
如下是 _FutureListener#handleValue
的代码处理,其中 _onValue
是 callback
成员函数 ,也就是用户在 _Future#then
中传入的第一参。sourceResult
是 _Future#_complete
中传入的结果。这两者将作为参数,被 _zone
对象通过 runUnary
执行。
---->[_FutureListener#_onValue]----
@pragma("vm:recognized", "other")
@pragma("vm:never-inline")
FutureOr<T> handleValue(S sourceResult) {
return _zone.runUnary<FutureOr<T>, S>(_onValue, sourceResult);
}
---->[_FutureListener#_onValue]----
FutureOr<T> Function(S) get _onValue {
assert(handlesValue);
return unsafeCast<FutureOr<T> Function(S)>(callback);
}
4. 梳理一下当前的 Zone 对象
在 _FutureListener
中获取的 _zone
是 _Future
对象持有的 _zone
。因为 result
对象是 _Future
类型的,在 _FutureListener
构造时赋值。如下是 _Future#then
中的处理 :
_Future
默认构造中使用的是 Zone._current
为 _zone
赋值。
---->[_FutureListener#_zone]----
_Zone get _zone => result._zone;
---->[_Future#_zone]----
_Future() : _zone = Zone._current;
Zone#_current
默认是一个 _RootZone
类型的常量 _rootZone
。
---->[Zone#_zone]----
static _Zone _current = _rootZone;
const _Zone _rootZone = const _RootZone();
_current
是 _Zone
的一个静态私有成员,所以它是可以变化的,由于是私有,它不能再外界被更改。所以在本文件中可以搜索其被赋值的场合。如下,在 _enter
和 _leave
方法中会对 _current
成员进行更改,表示进入和离开领域。
另外,一个场合是在处理未捕获异常时,可能对 _current
成员进行修改:
Zone
的 runUnary
方法,有两个参数,根据注释可以知道,该方法会在该 zone
中,将 argument
作为入参触发 action
函数。
---->[Zone#runUnary]----
/// Executes the given [action] with [argument] in this zone.
///
/// As [run] except that [action] is called with one [argument] instead of
/// none.
R runUnary<R, T>(R action(T argument), T argument);
Zone
是一个抽象类,_Zone
实现 Zone
接口,本身也是抽象类。_RootZone
继承自 _Zone
,是实现类。所以它必然要实现 runUnary
的抽象方法。
runUnary
在 _RootZone
中的实现如下,如果 Zone#_current
是 _rootZone
会直接触发 f
回调,并将 arg
作为参数,runUnary
的返回值即为入参函数的返回值。
如果 Zone#_current
非 _rootZone
时, 会触发 _rootRunUnary
。在其中也会触发 f
函数,且触发前后会分别执行 _enter
和 _leave
。这表示 f
函数执行期间 Zone#_current
会保持是 _rootZone
。
到这里 Future
对象的 then
监听的触发流程就非常清晰的。 Future#then
中设置回调函数,种下一个 因
。 Future#_complete
中发送通知,给出一个 果
,触发回调。稍微复杂一点的是其中 _FutureListener
的链表结构,以及通过 Zone
对象执行回调函数。关于 Zone
对象的知识,是比较复杂的,这里先简单了解一下,在 Future
中 Zone
的主要用途在代码上来看,是通过 runUnary
触发回调。
二、探索任务完成器 Completer
从上面可以看出 Future
本身只是封装了一套 监听 - 通知
的机制。比如 then
参数监听的事件,通过 _complete
可以触发通知,触发监听的回调。但何时触发 _complete
还是要 受制于人
的,比如延迟的异步任务,需要 Timer
对象的延时回调来触发 _complete
。
由于 _complete
是私有方法,这就导致我们无法操作 Future
的完成状态。当需要灵活控制 Future
对象完成状态的场景时,我们就需要 Completer
类的帮助,但这个场景是比较少见的。
1. 认识 Completer 类
Completer
类本身非常简单,核心成员是 Future
类型的 future
成员。并有两个抽象方法 complete
用于完成任务,completeError
用于错误完成任务。
也就是说 Completer
本质上只是对 Future
对象的一层封装,通过 Completer
提供的 API
来操作 future
成员而已。所以不用觉得 Completer
是什么高大上的东西。
Completer
本身是抽象类,其通过了工厂构造方法,返回的是 _AsyncCompleter
实现类。也就是说,如果直接通过 Completer()
创建对象,其运行时类型为 _AsyncCompleter
。
Future<int> foo(){
Completer<int> _completer = Completer();
return _completer.future;
}
_AsyncCompleter
集成自 _Completer
,其中只实现了 complete
和 _completeError
两个方法。 complete
方法在触发 future
对象的 _asyncComplete
方法,最后也会触发 _completeWithValue
方法向监听者发送通知,触发回调。
所以 future
成员的实例化一定是在 _Completer
类中实现的。如下,_Completer
继承自 Completer
,其中 future
成员是通过 _Future
直接构造的。
2. Completer 类的作用
总的来看, Completer
的唯一价值是可以让使用者控制 future
对象完成的时机。而这个功能在绝大多数的场景中都是不需要的,因为对于异步任务而言,我们期待任务完成的时机,发送任务的机体是被动的。
如下 delay3s
可以实现延迟 3s
的异步任务,但是这和 Future.delayed
在本质上并没有任何区别,只会把简单的事情搞复杂。所以,没有控制 future
对象完成的时机场合,都不需要使用 Completer
来自找麻烦。
Future<int> delay3s(){
Completer<int> completer = Completer();
Timer(const Duration(seconds:3 ),(){
completer.complete(49);
});
return completer.future;
}
下面看一下源码中对 Completer
的一处使用场景体会一下。在 RefreshIndicator
组件的实现中,对应的状态类 RefreshIndicatorState
使用了 Completer
。如下所示,其中定义的 _pendingRefreshFuture
对象,是由 Completer
创建的。
---->[RefreshIndicatorState]---
late Future<void> _pendingRefreshFuture;
---->[RefreshIndicatorState#_show]---
final Completer<void> completer = Completer<void>();
_pendingRefreshFuture = completer.future;
如下所示,refreshResult
是使用者传入的 Future
对象,在其完成之后,需要 mounted
且模式是 refreash
时,才会调用 completer.complete()
。像这种需要在某些特点场合下,需要控制任务完成的时机,是 Completer
的用武之地。
_pendingRefreshFuture
将作为 show
方法的返回值,这样 show
方法这个异步任务的完成时机,即 completer.complete()
触发的时机。这相当于在 RefreshIndicatorState
中,创建了一个 Future
对象,并手动在恰当的时机宣布完成。
三、 Future 中的四个静态方法
在第二篇初识 Future
时,我们知道 Future
中有几个静态方法,那时候没有介绍,在这里说明一下。静态方法通过类名进行调用,是完成特定任务的工具方法,在使用上是比较方便的。
1. Future#await 方法
从方法定义上来看,Future#await
方法需要传入 T
泛型 Future
对象的列表,返回值泛型为是 T
型结果数据列表的 Future
对象。
也就是说,Future#await
可以同时分发多个异步任务。如下所示,delayedNumber
和 delayedString
是两个延迟异步任务。通过 Future.wait
同时分发四个任务,从打印结果上可以看出,总耗时是 3s
,结果的顺序是 Future
列表中任务的顺序。
void main() async {
int start = DateTime.now().millisecondsSinceEpoch;
List<dynamic> value = await Future.wait<dynamic>([
delayedNumber(1),
delayedString("a"),
delayedNumber(1),
delayedString('b'),
]);
int cost = DateTime.now().millisecondsSinceEpoch-start;
print("cost:${cost/1000} s, value:$value"); // [2, result]
}
Future<int> delayedNumber(int num) async {
await Future.delayed(const Duration(seconds: 3));
return 2;
}
Future<String> delayedString(String value) async {
await Future.delayed(const Duration(seconds: 2));
return value;
}
这个静态方法适合在需要多个不相关的异步任务 同时分发
的场合,否则要写对四个 Future
进行监听,代码处理时就会非常复杂。最终的总耗时是这些任务中耗时最长的任务,不是所有任务的总和。
2. Future#any 方法
同样,Future#any
方法也需要传入 T
泛型 Future
对象的列表,但返回值是 T
泛型的 Future
对象。也就是说,该方法只允许有一个完成者,从表现上来看。它会返回第一个 完成
的异步任务,无论成败。
比如下面四个异步任务中, delayedString("a")
耗时 2s
,最快完成,然后被返回。
void main() async {
int start = DateTime.now().millisecondsSinceEpoch;
dynamic value = await Future.any<dynamic>([
delayedNumber(1),
delayedString("a"),
delayedNumber(3),
delayedString('b'),
]);
int cost = DateTime.now().millisecondsSinceEpoch-start;
print("cost:${cost/1000} s, value:$value"); // [2, result]
}
Future<int> delayedNumber(int num) async {
await Future.delayed(const Duration(seconds: 3));
return num;
}
Future<String> delayedString(String value) async {
await Future.delayed(const Duration(seconds: 2));
return value;
}
多个任务中取最先完成的任务结果,其余任务作废,感觉 any
方法的使用场景不是很常见。了解一下即可,说不定什么时候就有同类竞争的任务需求呢。
3. Future#doWhile 方法
Future.doWhile
可以循环执行一个方法,直到该方法返回 false
。如下所示,action
就是循环体,其中逻辑为 : 延迟 1s
后 value
自加,如果值为 3
返回 false
。
int value = 0;
void main() {
Future task = Future.doWhile(action);
task.then((_){
print('Finished with $value');
});
}
FutureOr<bool> action() async{
await Future.delayed(const Duration(seconds: 1));
value++;
if (value == 3) {
return false;
}
return true;
}
Future.doWhile
方法的使用场景也比较特殊,当希望循环执行一些异步任务时,可以尝试一下。有人可能觉得直接用 while
循环不比这简单易懂吗?因为 Future.doWhile
返回的是 Future
对象,我们可以通过它进行监听任务结束的执行情况,还是有些所势的。
4. Future#forEach 方法
Future#forEach
入参是 可迭代对象
和 元素操作回调
,很自然地可以想到它的作用是对可迭代对象进行 "异步加工"
。从源码实现来看,通过刚才的 doWhile
方法对列表进行遍历,在 646
行使用 action
回调对元素进行处理。
如下测试在,对 [0,1,2,3,4,5]
列表通过 forEach
进行处理,遍历期间触发 action
,每次延迟触发一秒。同样,感觉 Future.forEach
方法的使用场景也比较特殊,没什么太大的用处。
void main() {
Future task = Future.forEach<int>([0,1,2,3,4,5], action);
task.then((value){
print('Finished $value');
});
}
FutureOr<void> action(int element) async{
await Future.delayed(const Duration(seconds: 1));
int result = element*element;
print(result);
}
四、 Future 与 微任务
对于 Dart
、JavaScript
这种想在单线程中实现异步的语言,就脱离不了 事件循环机制 - Event Loop
,本篇并不对这个概念进行展开。先介绍一下在事件循环中的两类事件。
1. 认识微任务 microtask
这里再强调一下,Future
本身只是封装了一套 监听 - 通知
的机制,并非异步触发的核心角色。 如下 Future
中提供了 microtask
构造,其中使用了 scheduleMicrotask
方法,传入一个回调,并且在回调在触发 _complete
进行完成通知。
可以看出 scheduleMicrotask
和 Timer
是处于一个层级的,它们是触发异步任务的主要角色。另外在 Future
的默认构造在,使用 Timer.run
传入一个回调,并且在回调在触发 _complete
进行完成通知:
而 Timer.run
本质上就是一个 0s
定时器:
static void run(void Function() callback) {
new Timer(Duration.zero, callback);
}
使用可以看出 Future
构造只是个 电视剧外壳
,用于封装操作。其实现异步的核心是 Timer
和 scheduleMicrotask
。到这里,我们应该抛除 Future
的外壳,通过下一层来一窥本质,所以就不再以 Future
为探索的焦点。
2. 微任务回调与 Timer 回调的异步性
scheduleMicrotask
是定义在 dart:async
包中的全局方法,其中可以传入一个回调函数。如下所示,2
没有阻塞 3
、4
打印方法的执行,说明入参的该回调函数是 异步触发
的。
void main(){
print("done==1");
scheduleMicrotask((){
print("done==2");
});
print("done==3");
print("done==4");
}
---->[日志]----
done==1
done==3
done==4
done==2
从效果上来看 Timer.run
和 scheduleMicrotask
效果类似,都可以让传入的回调 异步触发
。
void main(){
print("done==1");
Timer.run((){
print("done==2");
});
print("done==3");
print("done==4");
}
---->[日志]----
done==1
done==3
done==4
done==2
3. Timer 和 scheduleMicrotask 异步任务的区别
这两种方式在触发的本质上还是有很大区别的,在下一篇探讨 事件循环机制 - Event Loop
时,会进行细致地分析。现在,我们先从 表象
上来看一下两者的区别:如下测试在是 7
个打印任务,其中 2
、3
通过 Timer.run
异步触发;4
、5
通过 scheduleMicrotask
异步触发。
void main(){
print("done==1");
Timer.run(()=> print("done==2"));
Timer.run(()=> print("done==3"));
scheduleMicrotask(()=> print("done==4"));
scheduleMicrotask(()=> print("done==5"));
print("done==6");
print("done==7");
}
打印日志如下,可以看出虽然 scheduleMicrotask
设置回调的代码,在 Timer.run
之后,但优先级是 scheduleMicrotask
的处理较高。在同级的情况下,先加入的先执行,其实从这里就可以看出一些 任务队列
的身影。
在 scheduleMicrotask
方法注释中,有一个很有意思的小例子,可以很形象法地体现出 scheduleMicrotask
优先级高于 Timer.run
。如下所示,先在 Timer.run
回调中打印 executed
, 然后定义 foo
方法,在其中通过 scheduleMicrotask
执行 foo
。这样 微任务队列
就永远不会停下, Timer.run
虽然是 0 s
之后回调,但永无回调时机。
main() {
Timer.run(() { print("executed"); }); // Will never be executed.
foo() {
scheduleMicrotask(foo); // Schedules [foo] in front of other events.
}
foo();
}
本文到这里,对 Future
类的分析已经非常全面了,其中最重要的一点是: Future
本身只是封装了一套 监听 - 通知
的机制。在 Future
的表象之下,隐藏着 Timer
和 scheduleMicrotask
, 以及其下的整个 事件循环机制 - Event Loop
。下一篇,将从 Timer
和 scheduleMicrotask
入手,揭开其背后秘密,最终你会发现 万物同源,天下大同
。那本文就到这里,谢谢观看 ~
转载自:https://juejin.cn/post/7152303127494918180