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

3,406 阅读14分钟

本文为稀土掘金技术社区首发签约文章,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 回调的契机。

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

image.png


如下是 _FutureListener#handleValue 的代码处理,其中 _onValuecallback 成员函数 ,也就是用户在 _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 中的处理 :

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<int> foo(){
  Completer<int> _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<void> _pendingRefreshFuture;

---->[RefreshIndicatorState#_show]---
final Completer<void> completer = Completer<void>();
_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

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 对象。也就是说,该方法只允许有一个完成者,从表现上来看。它会返回第一个 完成 的异步任务,无论成败。

image.png

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

image.png

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 就是循环体,其中逻辑为 : 延迟 1svalue 自加,如果值为 3 返回 false

image.png

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

image.png

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

image.png

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 与 微任务

对于 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 入手,揭开其背后秘密,最终你会发现 万物同源,天下大同 。那本文就到这里,谢谢观看 ~