Flutter/Dart中的异步

前言

咱们所熟悉的前端开发框架大都是事件驱动的。事件驱动意味着你的程序中必然存在事件循环和事件队列。事件循环会不停的从事件队列中获取和处理各类事件。也就是说你的程序必然是支持异步的。前端

在Android中这样的结构是Looper/Handler;在iOS中是RunLoop;在JavaScript中是Event Loop。面试

一样的Flutter/Dart也是事件驱动的,也有本身的Event Loop。并且这个Event Loop和JavaScript的很像,很像。(毕竟Dart是想替换JS来着)。下面咱们就来了解一下Dart中的Event Loop。网络

Dart的Event Loop

Dart的事件循环以下图所示。和JavaScript的基本同样。循环中有两个队列。一个是微任务队列(MicroTask queue),一个是事件队列(Event queue)。框架

  • 事件队列包含外部事件,例如I/O, Timer,绘制事件等等。
  • 微任务队列则包含有Dart内部的微任务,主要是经过scheduleMicrotask来调度。

Dart的Event Loop

Dart的事件循环的运行遵循如下规则:异步

  • 首先处理全部微任务队列里的微任务。
  • 处理完全部微任务之后。从事件队列里取1个事件进行处理。
  • 回到微任务队列继续循环。

注意第一步里的全部,也就是说在处理事件队列以前,Dart要先把全部的微任务处理完。若是某一时刻微任务队列里有8个微任务,事件队列有2个事件,Dart也会先把这8个微任务所有处理完再从事件队列中取出1个事件处理,以后又会回到微任务队列去看有没有未执行的微任务。async

总而言之,就是对微任务队列是一次性所有处理,对于事件队列是一次只处理一个。函数

这个流程要清楚,清楚了才能理解Dart代码的执行顺序。oop

异步执行

那么在Dart中如何让你的代码异步执行呢?很简单,把要异步执行的代码放在微任务队列或者事件队列里就好了。源码分析

  • 能够调用scheduleMicrotask来让代码以微任务的方式异步执行
scheduleMicrotask((){
        print('a microtask');
    });
复制代码
  • 能够调用Timer.run来让代码以Event的方式异步执行
Timer.run((){
       print('a event');
   });
复制代码

好了,如今你知道怎么让你的Dart代码异步执行了。看起来并非很复杂,可是你须要清楚的知道你的异步代码执行的顺序。这也是不少前端面试时候会问到的问题。举个简单的例子,请问下面这段代码是否会输出"executed"?ui

main() {
     Timer.run(() { print("executed"); });  
      foo() {
        scheduleMicrotask(foo);  
      }
      foo();
    }
复制代码

答案是不会,由于在始终会有一个foo存在于微任务队列。致使Event Loop没有机会去处理事件队列。还有更复杂的一些例子会有大量的异步代码混合嵌套起来而后问你执行顺序是什么样的,这都须要按照上述Event Loop规则仔细去分析。

和JS同样,仅仅使用回调函数来作异步的话很容易陷入“回调地狱(Callback hell)”,为了不这样的问题,JS引入了Promise。一样的, Dart引入了Future

Future

要使用Future的话须要引入dart.async

import 'dart:async';
复制代码

Future提供了一系列构造函数供你选择。

建立一个马上在事件队列里运行的Future:

Future(() => print('马上在Event queue中运行的Future'));
复制代码

建立一个延时1秒在事件队列里运行的Future:

Future.delayed(const Duration(seconds:1), () => print('1秒后在Event queue中运行的Future'));
复制代码

建立一个在微任务队列里运行的Future:

Future.microtask(() => print('在Microtask queue里运行的Future'));
复制代码

建立一个同步运行的Future:

Future.sync(() => print('同步运行的Future'));
复制代码

对,你没看错,同步运行的。

这里要注意一下,这个同步运行指的是构造Future的时候传入的函数是同步运行的,这个Future经过then串进来的回调函数是调度到微任务队列异步执行的。

有了Future以后, 经过调用then来把回调函数串起来,这样就解决了"回调地狱"的问题。

Future(()=> print('task'))
    .then((_)=> print('callback1'))
    .then((_)=> print('callback2'));
复制代码

在task打印完毕之后,经过then串起来的回调函数会按照连接的顺序依次执行。 若是task执行出错怎么办?你能够经过catchError来链上一个错误处理函数:

Future(()=> throw 'we have a problem')
      .then((_)=> print('callback1'))
      .then((_)=> print('callback2'))
      .catchError((error)=>print('$error'));
复制代码

上面这个Future执行时直接抛出一个异常,这个异常会被catchError捕捉到。相似于Java中的try/catch机制的catch代码块。运行后只会执行catchError里的代码。两个then中的代码都不会被执行。

既然有了相似Java的try/catch,那么Java中的finally也应该有吧。有的,那就是whenComplete:

Future(()=> throw 'we have a problem')
    .then((_)=> print('callback1'))
    .then((_)=> print('callback2'))
    .catchError((error)=>print('$error'))
    .whenComplete(()=> print('whenComplete'));
复制代码

不管这个Future是正常执行完毕仍是抛出异常,whenComplete都必定会被执行。

以上就是对Future的一些主要用法的介绍。Future背后的实现机制仍是有一些复杂的。这里先列几个来自Dart官网的关于Future的烧脑说明。你们先感觉一下:

  1. 你经过then串起来的那些回调函数在Future完成的时候会被当即执 行,也就是说它们是同步执行,而不是被调度异步执行。
  2. 若是Future在调用then串起回调函数以前已经完成,
    那么这些回调函数会被调度到微任务队列异步执行。
  3. 经过Future()Future.delayed()实例化的Future不会同步执行,它们会被调度到事件队列异步执行。
  4. 经过Future.value()实例化的Future会被调度到微任务队列异步完成,相似于第2条。
  5. 经过Future.sync()实例化的Future会同步执行其入参函数,而后(除非这个入参函数返回一个Future)调度到微任务队列来完成本身,相似于第2条。

从上述说明能够得出结论,Future中的代码至少会有一部分被异步调度执行的,要么是其入参函数和回调被异步调度执行,要么就只有回调被异步调度执行。

不知道你们注意到没有,经过以上那些Future构造函数生成的Future对象其实控制权不在你这里。它何时执行完毕只能等系统调度了。你只能被动的等待Future执行完毕而后调用你设置的回调。若是你想手动控制某个Future怎么办呢?请使用Completer

Completer

这里就举个Completer的例子吧

// 实例化一个Completer
var completer = Completer();
// 这里能够拿到这个completer内部的Future
var future = completer.future;
// 须要的话串上回调函数。
future.then((value)=> print('$value'));

//作些其它事情 
...
// 设置为完成状态
completer.complete("done");

复制代码

上述代码片断中,当你建立了一个Completer之后,其内部会包含一个Future。你能够在这个Future上经过then, catchErrorwhenComplete串上你须要的回调。拿着这个Completer实例,在你的代码里的合适位置,经过调用complete函数便可完成这个Completer对应的Future。控制权彻底在你本身的代码手里。固然你也能够经过调用completeError来以异常的方式结束这个Future

总结就是:

  • 我建立的,完成了调个人回调就好了: 用 Future
  • 我建立的,得我来结束它: 用Completer

Future相对于调度回调函数来讲,缓减了回调地狱的问题。可是若是Future要串起来的的东西比较多的话,代码仍是会可读性比较差。特别是各类Future嵌套起来,是比较烧脑的。

因此能不能更给力一点呢?能够的!JavaScript有 async/await,Dart也有。

async/await

asyncawait是什么?它们是Dart语言的关键字,有了这两个关键字,可让你用同步代码的形式写出异步代码。啥意思呢?看下面这个例子:

foo() async {
  print('foo E');
  String value = await bar();
  print('foo X $value');
}

bar() async {
  print("bar E");
  return "hello";
}

main() {
  print('main E');
  foo();
  print("main X");
}
复制代码

函数foo被关键字async修饰,其内部的有3行代码,看起来和普通的函数没什么两样。可是在第2行等号右侧有个await关键字,await的出现让看似会同步执行的代码裂变为两部分。以下图所示:

async await
绿框里面的代码会在 foo函数被调用的时候同步执行,在遇到 await的时候,会立刻返回一个 Future,剩下的红框里面的代码以 then的方式链入这个 Future被异步调度执行。

上述代码运行之后在终端会输出以下:

output
可见 print('foo X $value')是在 main执行完毕之后才打印出来的。的确是异步执行的。

而以上代码中的foo函数能够以Future方式实现以下,二者是等效的

foo() {
  print('foo E');
  return Future.sync(bar).then((value) => print('foo X $value'));
}
复制代码

await并不像字面意义上程序运行到这里就停下来啥也不干等待Future完成。而是马上结束当前函数的执行并返回一个Future。函数内剩余代码经过调度异步执行。

  • await只能在async函数中出现。
  • async函数中能够出现多个await,每碰见一个就返回一个Future, 实际结果相似于用then串起来的回调。
  • async函数也能够没有await, 在函数体同步执行完毕之后返回一个Future

使用asyncawait还有一个好处是咱们能够用和同步代码相同的try/catch机制来作异常处理。

foo() async {
  try {
    print('foo E');
    var value = await bar();
    print('foo X $value');
  } catch (e) {
    // 同步执行代码中的异常和异步执行代码的异常都会被捕获
  } finally {
    
  }
}
复制代码

在平常使用场景中,咱们一般利用asyncawait来异步处理IO,网络请求,以及Flutter中的Platform channels通讯等耗时操做。

总结

本文大体介绍了Flutter/Dart中的异步运行机制,从异步运行的基础(Event Loop)开始,首先介绍了最原始的异步运行机制,直接调度回调函数;到Future;再到 asyncawait。了解了Flutter/Dart中的异步运行机制是如何一步一步的进化而来的。对于一直从事Native开发,不太了解JavaScrip的同窗来说,这个异步机制和原生开发有很大的不一样,须要多多动手练习,动脑思考才能适应。本文中介绍的相关知识点较为粗浅,并无涉及dart:async中关于Future实现的源码分析以及Stream等不太经常使用的类。这些若是你们想了解一下的话我会另写文章来介绍一下。