那些你不知道的Dart细节之带你透彻理解异步

1,498 阅读9分钟

那些你不知道的Dart细节之带你透彻理解异步

前言

上周一口气写了五篇Dart基础文章,建议看这篇文章之前先看一下前几篇文章:

那些你不知道的Dart细节之变量

那些你不知道的Dart细节之内置类型

那些你不知道的Dart细节之函数(方法)

那些你不知道的Dart细节之操作符、流程控制语句、异常

那些你不知道的Dart细节之类的点点滴滴

那些你不知道的Dart细节之泛型和库

好了,废话不多说,开始进入今天的主题吧!

async和await

开始说这两个关键字之前我觉得有必要提一下:在Dart中没有子线程一说,所有代码都是在一条主线上运行的,所以需要用异步来实现一些耗时操作。(如果非要开启多线程需要使用隔离,这里不做叙述)

来说一下这两个关键字吧,async用来修饰方法,需要写在方法括号的后面,await写在方法里面,这里要注意:await关键字必须在async函数内部使用,不然会报错。await表达式可以使用多次。,这里其实很好理解:都不是异步方法了你还等待啥啊?下面看一个简单的样例吧:

void main() {
  getName1();
  getName2();
  getName3();
}

getName1() async {
  await getStr1();
  await getStr2();
  print('getName1');
}

getStr1() {
  print('getStr1');
}

getStr2() {
  print('getStr2');
}

getName2() {
  print('getName2');
}

getName3() {
  print('getName3');
}

上面这段代码并不长,大家可以猜一下打印出来的值。

咱们来一步一步分析吧,后面再贴出来答案,看看大家猜的对不对。

首先执行getName1(),执行的时候发现这个方法是async的方法,继续执行,执行到方法中第一行的时候,发现调用了一个getStr1()方法,而且这个方法使用了await来修饰,表示需要等待执行,重点来了:当遇到await的时候会执行完这一行,打印出了getStr1,之后立即返回一个Future(void)对象(上面的代码中省略了这个,写代码时推荐加上,方便代码阅读理解),然后将这个方法中剩余的代码放入了事件队列,接着往下执行getName2()和getName3(),分别打印出了getName2和getName3,刚才也说过,在Dart中只有一个main线程一桶到底,还有一个事件队列,现在main线程中都已经执行完毕,但是事件队列中还有东西,继续执行getStr2(),执行的时候发现还是await,再进行等待,等待执行完成后打印getStr2,最后再打印getName1

下面是打印出的结果:

getStr1
getName2
getName3
getStr2
getName1

和大家猜的一样吗?为什么会这样打印上面已经进行了分析。下面咱们看一下其他的几个关键字。

then,catchError,whenComplete

上面分析中说过,async方法中遇到await时即会返回一个Future对象,从字面上也能知道这个一个未来的值,那么肯定需要等待完成之后才能获取到里面的值。then关键字的意思就是获取等待执行完毕之后返回的值,光说感觉说不明白,还是来看一段代码吧:

void main() {
  new Future(() => futureTask())//异步任务的函数
      .then((i) => "result:$i")//任务执行完后的子任务
      .then((m) => print(m)); //其中m为上个任务执行完后的返回的结果
}

futureTask() {
  return 10;
}

上面这段代码中只有一个打印,下面是打印出的值:

result:10

为什么不只显示10呢?因为第二次then的时候参数m是第一次then返回的值,而不是futureTask()返回的10这里应该不难理解。

在then之后还可以抛异常,下面来抛一个异常来看看:

new Future(() => futureTask())//异步任务的函数
      .then((i) => "result:$i")//任务执行完后的子任务
      .then((m) => print(m)) //其中m为上个任务执行完后的返回的结果
      .then((_) => Future.error("出错了"));

从上面代码中可以知道抛异常的方法Future.error("出错了"),来看一下执行情况:

result:10
Unhandled exception:
出错了
#0      _rootHandleUncaughtError.<anonymous closure> (dart:async/zone.dart:1114:29)
#1      _microtaskLoop (dart:async/schedule_microtask.dart:43:21)
#2      _startMicrotaskLoop (dart:async/schedule_microtask.dart:52:5)
#3      _Timer._runTimers (dart:isolate-patch/timer_impl.dart:393:30)
#4      _Timer._handleMessage (dart:isolate-patch/timer_impl.dart:418:5)
#5      _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:174:12)

既然可以抛异常了,当然也可以catch异常,直接使用catchError关键字来捕获一下:

new Future(() => futureTask())//异步任务的函数
      .then((i) => "result:$i")//任务执行完后的子任务
      .then((m) => print(m)) //其中m为上个任务执行完后的返回的结果
      .then((_) => Future.error("出错了"))
      .catchError(print);

写法很简单,直接参照上面代码写即可,下面是运行结果:

result:10
出错了

👌,已经捕获了一场,需要做什么事的可以在里面进行操作了。

对了,标题中还有一个关键字没说,whenComplete,这个关键字的意思简单,指所有任务完成后的回调函数,使用也很简单,直接看代码:

new Future(() => futureTask())//异步任务的函数
      .then((i) => "result:$i")//任务执行完后的子任务
      .then((m) => print(m)) //其中m为上个任务执行完后的返回的结果
      .then((_) => Future.error("出错了"))
      .catchError(print)
      .whenComplete(() => print("whenComplete"));//所有任务完成后的回调函数

看一下运行结果:

result:10
出错了
whenComplete

很简单对吧?下面说的就有点恶心了。。。🤢

Event-Looper

上面两张图说的就是Dart的Event-Looper。其实也不难理解:

  • 一个消息循环的职责就是不断从消息队列中取出消息并处理他们直到消息队列为空。
  • 消息队列中的消息可能来自用户输入,文件I/O消息,定时器等。例如上图的消息队列就包含了定时器消息和用户输入消息。
  • Dart中的Main Isolate只有一个Event Looper,但是存在两个Event Queue: Event Queue以及Microtask Queue。

简单了解一下就行,只要记着它就是一个消息队列,如果有值就一直循环取出进行处理就够了。第三点说的事件队列和微队列在下面说吧,这里没有例子不太好理解。

new Future()

到这里本文的核心知识点就来了,首先来一张我自己的手绘图吧:

嗯。。。不要在意画的,看重点:Dart中方法执行无外乎这三个地方,主线程main,事件队列(就是上面说的 Event Queue)和微队列(上面的Microtask Queue),执行顺序不太一样样,先执行main线程,然后是微队列,最后是事件队列。这里有一个坑,咱们从下面的代码中来彻底理解一下:

void main(){
  testFuture();
}
void testFuture() {
  Future f = new Future(() => print('f1'));
  Future f1 = new Future(() => null);
  Future f2 = new Future(() => null);
  Future f3 = new Future(() => null);
  f3.then((_) => print('f2'));
  f2.then((_) {
    print('f3');
    new Future(() => print('f4'));
    f1.then((_) {
      print('f5');
    });
  });
  f1.then((m) {
    print('f6');
  });
  print('f7');
}

还是和上面一样,大家来猜一下运行结果,我先来分析,最后我会把运行结果贴出来,不想看分析的可以直接看运行结果查看一下自己的判断是否正确。

首先,main中执行了方法testFuture(),在testFuture()方法中在main线程的只有print('f7'),其他的都在事件队列,所以先打印了f7***,main线程已经执行完毕,再来看事件队列,执行事件队列之前需要查看一下微队列中是否有东西,如果微队列中有东西的话需要先执行*(这就是上面所说的坑),来看一下现在的队列中都有啥吧:

事件队列中现在依次放着f、f1、f2和f3,f直接打印了,ok,这里打印出了f1f1、f2和f3还在事件队列中,这里有一些迷惑,代码中我先放的是f3的then,但是执行的时候并不是先执行f3的then,而是根据事件队列中的顺序来执行的,事件队列中的f已经执行完毕,接下来该执行f1,f1打印出了f6,f1也执行完毕,该执行f2,f2的then中东西就比较多了,这里也有今天的重中之重。执行f2的then,先打印出了f3,然后又新增了一个Future,记着,这里不是直接执行,而是将新增的f4也放入了事件队列中,再往下执行,用到了f1的then,但是f1咱们刚才已经执行完毕了,这里又对f1进行了调用,那么这个时候就需要把f1放入微队列中,再来看一下现在的队列中都有啥:

还记得刚才说的吗?本来f2咱们已经执行完成该执行f3了,但是现在微队列中有了东西,咱们就需要先执行微队列中的东西,OK,又打印出了f5,接着执行事件队列中的f3,打印出了f2,最后执行f4,打印出了f4

到这里就基本分析完了,看完分析的应该已经知道打印的值了,来看一下吧:

f7
f1
f6
f3
f5
f2
f4

和咱们分析的一样,这里需要注意的就是微队列和事件队列的关系

scheduleMicrotask()

上面代码中f1进入了微队列是因为执行完毕之后再执行,故而放入了微队列中,这是被动进去的,就不能主动放入微队列中吗?当然可以,使用scheduleMicrotask()就可以放入微队列中。

经过上面的一段代码相信大家对异步已经有了一定了解,那么下面再来一段稍微复杂点的加上了scheduleMicrotask()的代码:

void main(){
  testScheduleMicrotask();
}
void testScheduleMicrotask(){
  scheduleMicrotask(() => print('s1'));//微任务
  //delay 延迟
  new Future.delayed(new Duration(seconds: 1), () => print('s2'));

  new Future(() => print('s3')).then((_) {
    print('s4');
    scheduleMicrotask(() => print('s5'));
  }).then((_) => print('s6'));

  new Future(() => print('s7'));

  scheduleMicrotask(() => print('s8'));

  print('s9');
}

还是和上面一样,还是同样的mian线程、事件队列和微队列,还是先来分析代码,最后再贴出运行结果。

上面代码中main线程中只有一个print('s9'),那么就先打印s9,然后有两个主动创建的微队列,s2进行了延迟,这里注意,进行了延迟直接放入队列的末尾,所以说最后一个打印的是s2。然后s3放入了事件队列的第一个,s7放入了第二个,再来看一下现在的队列吧:

在执行事件队列前先看一下微队列中是否有东西,ok,微队列中有,先来执行微队列,分别打印出了s1和s8,再来执行事件队列s3,先打印出了s3,再打印出了s4,又出现了微队列s5,这里注意:先将s5放入微队列中,并不是直接执行,而是执行完s3的事件队列之后,在执行s7之前再去查看微队列是否有值,来看一下现在的队列样式:

所以这里又打印了s6,再打印了微队列中的s5,之后执行事件队列中的s7,打印出了s7,最后再打印出延时操作的s2

好了,分析完毕,运行结果已经出来了,来看一下吧:

s9
s1
s8
s3
s4
s6
s5
s7
s2

这里有几点需要大家注意:

  • 如果可以,尽量将任务放入event队列中
  • 使用Future的then方法或whenComplete方法来指定任务顺序
  • 为了保持你app的可响应性,尽量不要将大计算量的任务放入这两个队列
  • 大计算量的任务放入额外的isolate中(isolate就是上面提到了隔离,这里不做解释。)

总结

好了,到这里本篇文章就到尾声了。本篇文章带大家一起看了一下Dart中的异步执行顺序,只要理解了上面的两端代码,其实Dart的异步你已经基本掌握了,剩下的只有多写代码去练、去理解才行。这是我Dart的第七篇文章了,也是Dart基础的最后一篇文章,前六篇大家可以通过文章开头的链接去访问,如果文章对你有帮助,别忘记点赞关注;如果对文章有异议,可以在下面评论区指出来,共同学习进步,不胜感激。