【Flutter 异步编程 -伍】 | 深入剖析 Future 类源码实现


theme: cyanosis

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
张风捷特烈 - 出品

一、Future 中的监听与通知

在日常开发中,我们一般知道对 Future 对象通过 thenonError 设置回调方法进行监听。但很少有机会了解 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"); });

image.png

这里说明一个小细节:我们知道 Future 是一个抽象类,并不能直接实例化对象,但可以通过 factory 构造,创建 子类对象 返回。这里 Future.delayed 就是一个工厂构造,上图中 418 行会创建 _Future 对象 result ,在 430 行对 result 对象进行返回。


如下代码中 Future.delayed 方法没有第二参,说明构造时 computation 为空,走的是 421 行,触发 result_complete 方法表示任务完成。这可以说明一个问题: Future#_complete 方法是触发 Future#then 回调的契机。

dart Future delayedTask = Future.delayed(delay); delayedTask.then((value){ print("task2 done"); });

Future#_complete 方法中可以看出,其入参是 FutureOr<T> ,说明可以传入 Future 对象或 T 泛型对象。如果非 Future 对象,表示任务完成,会触发 _propagateToListeners 方法通知监听者们。

image.png


2. 探寻 Future 的回调监听

我们知道,Future 对象的完成时机,可以通过 then 方法中的回调进行监听。在运行时的实际类型是 _Future ,所以看一下它的 then 方法实现。如下所示,then 方法的第一参为回调函数,这里的函数名为 f

314 行会将 f 函数被注册到 currentZone 的回调中;如果 onError 非空,也会在 _registerErrorHandler 方法在,被注册到 currentZone 的回调在中。

image.png

最后会通过 _addListener 方法添加 _FutureListener 监听者对象,如下所示,可以推断出 _FutureListener 是一个链表结构,而且 _Future 类中持有链表的首节点 _resultOrListeners 。总而言之, _Future.then 中的主要工作是 注册回调添加监听者

image.png


如下是 _FutureListener.then 的构造代码,可以看出 _FutureListener 中有 callbackerrorCallback 两个函数对象,他们将在 then 构造中被赋值。结合 _Future#then 在创建 _FutureListener 的代码(323 行)可知,用户传入的回调 f 作为第一参,也就是为 _FutureListenercallback 对象赋值。

也就是说,_FutureListener#callback 被调用的场合,就是 then 中成功结果的回调时机。就相当于 把鱼钩投入水中 (设置回调),接下来探索一下 何时鱼会上钩(触发通知) 。

image.png

PS : 不知为何 Dart 中不允许对这块内容进行 断点调试日志打印 ,所以只能根据源码的功能、结合代码中的线索进行分析。如果有什么认知不对的地方,希望大家可以友善讨论。


3. 探寻 Future 的触发通知

从程序运行的逻辑上来看, Future#_complete 是触发 Future#then 中回调的原因。在 557 行所示,_propagateToListeners 方法在字面的意思上是通知监听者。下面来详细看一下:

image.png

551 行会先触发 _removeListeners 方法移除监听者,并返回 _FutureListener 对象。该对象将作为 _propagateToListeners 的第二入参,也就是需要被通知的监听者们。如下所示,_removeListeners 方法中会将 _Future_resultOrListeners 成员置空,也就是移除监听者的表现。 另外,返回值是通过 _reverseListeners 方法返回的 _FutureListener 对象:

image.png

_reverseListeners 方法是一个非常经典的单链表反转操作:比如进入方法时,current 是链表的首节点 A,且其后有 BC 节点;那么方法执行完后,链接结构就是 C 为首节点,其后是 BA 节点。最后一次 while 循环时,484prev 被赋值为 current,所以最终返回的 prev 对象就是首节点 C

image.png

从这里可以看出,_removeListeners 方法的作用是置空 _Future#_resultOrListeners ,并将监听者链表反序返回。

image.png


_propagateToListeners 是定义在 _Future 中的静态方法,从代码注释中能看出:它可以触发 listeners 的回调。其中有两个入参,其一是 _Future 对象,其二是 _FutureListener 对象。在该 _complete 中被调用时,第一参入参是 this 对象,第二入参是上面反序返回的监听者链表 首节点

image.png


_propagateToListeners 方法内定义了三个函数,handleWhenCompleteCallbackhandleValueCallbackhandleError 分别用于处理 完成正确结果异常

image.png

其中 then 中的结果回调对应的是 handleValueCallback

image.png

如下,在 handleValueCallback 中,listener 会触发 handleValue 方法。其中 sourceResult 就是d当前 _Future 对象的 _resultOrListeners 。如下 tag1 处,在 _complete 方法 _setValue 时会将结果赋值给 _resultOrListeners

``` ---->[propagateToListeners]---- final dynamic sourceResult = source.resultOrListeners;

---->[_complete]---- void _complete(FutureOr 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; } ```

image.png


如下是 _FutureListener#handleValue 的代码处理,其中 _onValuecallback 成员函数 ,也就是用户在 _Future#then 中传入的第一参。sourceResult_Future#_complete 中传入的结果。这两者将作为参数,被 _zone 对象通过 runUnary 执行。

``` ---->[FutureListener#onValue]---- @pragma("vm:recognized", "other") @pragma("vm:never-inline") FutureOr handleValue(S sourceResult) { return zone.runUnary , S>( onValue, sourceResult); }

---->[FutureListener#onValue]---- FutureOr Function(S) get _onValue { assert(handlesValue); return unsafeCast Function(S)>(callback); } ```


4. 梳理一下当前的 Zone 对象

_FutureListener 中获取的 _zone_Future 对象持有的 _zone 。因为 result 对象是 _Future 类型的,在 _FutureListener 构造时赋值。如下是 _Future#then 中的处理 :

image.png

_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 成员进行更改,表示进入和离开领域。

image.png

另外,一个场合是在处理未捕获异常时,可能对 _current 成员进行修改:

image.png


ZonerunUnary 方法,有两个参数,根据注释可以知道,该方法会在该 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 的抽象方法。

image.png

runUnary_RootZone 中的实现如下,如果 Zone#_current_rootZone 会直接触发 f 回调,并将 arg 作为参数,runUnary 的返回值即为入参函数的返回值。

image.png

如果 Zone#_current_rootZone 时, 会触发 _rootRunUnary 。在其中也会触发 f 函数,且触发前后会分别执行 _enter_leave 。这表示 f 函数执行期间 Zone#_current 会保持是 _rootZone

image.png

到这里 Future 对象的 then 监听的触发流程就非常清晰的。 Future#then 中设置回调函数,种下一个 Future#_complete 中发送通知,给出一个 ,触发回调。稍微复杂一点的是其中 _FutureListener 的链表结构,以及通过 Zone 对象执行回调函数。关于 Zone 对象的知识,是比较复杂的,这里先简单了解一下,在 FutureZone 的主要用途在代码上来看,是通过 runUnary 触发回调。


二、探索任务完成器 Completer

从上面可以看出 Future 本身只是封装了一套 监听 - 通知 的机制。比如 then 参数监听的事件,通过 _complete 可以触发通知,触发监听的回调。但何时触发 _complete 还是要 受制于人 的,比如延迟的异步任务,需要 Timer 对象的延时回调来触发 _complete

由于 _complete 是私有方法,这就导致我们无法操作 Future 的完成状态。当需要灵活控制 Future 对象完成状态的场景时,我们就需要 Completer 类的帮助,但这个场景是比较少见的。


1. 认识 Completer 类

Completer 类本身非常简单,核心成员是 Future 类型的 future 成员。并有两个抽象方法 complete 用于完成任务,completeError 用于错误完成任务。

image.png

也就是说 Completer 本质上只是对 Future 对象的一层封装,通过 Completer 提供的 API 来操作 future 成员而已。所以不用觉得 Completer 是什么高大上的东西。


Completer 本身是抽象类,其通过了工厂构造方法,返回的是 _AsyncCompleter 实现类。也就是说,如果直接通过 Completer() 创建对象,其运行时类型为 _AsyncCompleter

image.png

``` Future foo(){ Completer _completer = Completer(); return _completer.future; }

```

_AsyncCompleter 集成自 _Completer ,其中只实现了 complete_completeError 两个方法。 complete 方法在触发 future 对象的 _asyncComplete 方法,最后也会触发 _completeWithValue 方法向监听者发送通知,触发回调。

image.png

所以 future 成员的实例化一定是在 _Completer 类中实现的。如下,_Completer 继承自 Completer,其中 future 成员是通过 _Future 直接构造的。

image.png


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 _pendingRefreshFuture;

---->[RefreshIndicatorState#_show]--- final Completer completer = Completer (); _pendingRefreshFuture = completer.future;

```

如下所示,refreshResult 是使用者传入的 Future 对象,在其完成之后,需要 mounted 且模式是 refreash 时,才会调用 completer.complete() 。像这种需要在某些特点场合下,需要控制任务完成的时机,是 Completer 的用武之地。

image.png

_pendingRefreshFuture 将作为 show 方法的返回值,这样 show 方法这个异步任务的完成时机,即 completer.complete() 触发的时机。这相当于在 RefreshIndicatorState 中,创建了一个 Future 对象,并手动在恰当的时机宣布完成。

image.png


三、 Future 中的四个静态方法

在第二篇初识 Future 时,我们知道 Future 中有几个静态方法,那时候没有介绍,在这里说明一下。静态方法通过类名进行调用,是完成特定任务的工具方法,在使用上是比较方便的。

image.png


1. Future#await 方法

从方法定义上来看,Future#await 方法需要传入 T 泛型 Future 对象的列表,返回值泛型为是 T 型结果数据列表的 Future 对象。

image.png

也就是说,Future#await 可以同时分发多个异步任务。如下所示,delayedNumberdelayedString 是两个延迟异步任务。通过 Future.wait 同时分发四个任务,从打印结果上可以看出,总耗时是 3s ,结果的顺序是 Future 列表中任务的顺序。

image.png

```dart void main() async { int start = DateTime.now().millisecondsSinceEpoch; List value = await Future.wait ([ delayedNumber(1), delayedString("a"), delayedNumber(1), delayedString('b'), ]); int cost = DateTime.now().millisecondsSinceEpoch-start; print("cost:${cost/1000} s, value:$value"); // [2, result] }

Future delayedNumber(int num) async { await Future.delayed(const Duration(seconds: 3)); return 2; }

Future delayedString(String value) async { await Future.delayed(const Duration(seconds: 2)); return value; } ```

这个静态方法适合在需要多个不相关的异步任务 同时分发 的场合,否则要写对四个 Future 进行监听,代码处理时就会非常复杂。最终的总耗时是这些任务中耗时最长的任务,不是所有任务的总和。


2. Future#any 方法

同样,Future#any 方法也需要传入 T 泛型 Future 对象的列表,但返回值是 T 泛型的 Future 对象。也就是说,该方法只允许有一个完成者,从表现上来看。它会返回第一个 完成 的异步任务,无论成败。

image.png

比如下面四个异步任务中, delayedString("a") 耗时 2s ,最快完成,然后被返回。

image.png

``` void main() async { int start = DateTime.now().millisecondsSinceEpoch; dynamic value = await Future.any ([ delayedNumber(1), delayedString("a"), delayedNumber(3), delayedString('b'), ]); int cost = DateTime.now().millisecondsSinceEpoch-start;

print("cost:${cost/1000} s, value:$value"); // [2, result] } Future delayedNumber(int num) async { await Future.delayed(const Duration(seconds: 3)); return num; }

Future delayedString(String value) async { await Future.delayed(const Duration(seconds: 2)); return value; } ```

多个任务中取最先完成的任务结果,其余任务作废,感觉 any 方法的使用场景不是很常见。了解一下即可,说不定什么时候就有同类竞争的任务需求呢。


3. Future#doWhile 方法

Future.doWhile 可以循环执行一个方法,直到该方法返回 false。如下所示,action 就是循环体,其中逻辑为 : 延迟 1svalue 自加,如果值为 3 返回 false

image.png

``` int value = 0;

void main() { Future task = Future.doWhile(action); task.then((_){ print('Finished with $value'); }); }

FutureOr 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 回调对元素进行处理。

image.png

如下测试在,对 [0,1,2,3,4,5] 列表通过 forEach 进行处理,遍历期间触发 action ,每次延迟触发一秒。同样,感觉 Future.forEach 方法的使用场景也比较特殊,没什么太大的用处。

image.png

``` void main() { Future task = Future.forEach ([0,1,2,3,4,5], action); task.then((value){ print('Finished $value'); }); }

FutureOr action(int element) async{ await Future.delayed(const Duration(seconds: 1)); int result = element*element; print(result); } ```


四、 Future 与 微任务

对于 DartJavaScript 这种想在单线程中实现异步的语言,就脱离不了 事件循环机制 - Event Loop,本篇并不对这个概念进行展开。先介绍一下在事件循环中的两类事件。


1. 认识微任务 microtask

这里再强调一下,Future 本身只是封装了一套 监听 - 通知 的机制,并非异步触发的核心角色。 如下 Future 中提供了 microtask 构造,其中使用了 scheduleMicrotask 方法,传入一个回调,并且在回调在触发 _complete 进行完成通知。

image.png


可以看出 scheduleMicrotaskTimer 是处于一个层级的,它们是触发异步任务的主要角色。另外在 Future 的默认构造在,使用 Timer.run 传入一个回调,并且在回调在触发 _complete 进行完成通知: image.png

Timer.run 本质上就是一个 0s 定时器:

static void run(void Function() callback) { new Timer(Duration.zero, callback); }

使用可以看出 Future 构造只是个 电视剧外壳,用于封装操作。其实现异步的核心是 TimerscheduleMicrotask 。到这里,我们应该抛除 Future 的外壳,通过下一层来一窥本质,所以就不再以 Future 为探索的焦点。


2. 微任务回调与 Timer 回调的异步性

scheduleMicrotask 是定义在 dart:async 包中的全局方法,其中可以传入一个回调函数。如下所示,2 没有阻塞 34 打印方法的执行,说明入参的该回调函数是 异步触发 的。

``` void main(){ print("done==1"); scheduleMicrotask((){ print("done==2"); }); print("done==3"); print("done==4"); }

---->[日志]---- done==1 done==3 done==4 done==2 ```


从效果上来看 Timer.runscheduleMicrotask 效果类似,都可以让传入的回调 异步触发

``` 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 个打印任务,其中 23 通过 Timer.run 异步触发;45 通过 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 的处理较高。在同级的情况下,先加入的先执行,其实从这里就可以看出一些 任务队列 的身影。

image.png


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 的表象之下,隐藏着 TimerscheduleMicrotask , 以及其下的整个 事件循环机制 - Event Loop。下一篇,将从 TimerscheduleMicrotask 入手,揭开其背后秘密,最终你会发现 万物同源,天下大同 。那本文就到这里,谢谢观看 ~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值