Flutter学习-Dart异步编程

999 阅读10分钟

Flutter和Dart之间的关系

Flutter是基于Dart语言的移动UI框架。

Dart语言

Dart是谷歌开发的计算机编程语言。它被用于web、服务器、移动应用和物联网等领域的开发。

Flutter框架

Flutter是谷歌的移动UI框架,可以快速在iOS和Android上构建高质量的原生用户界面。

异步示例 - Future

1.使用Future来实现异步

String _data = '1';
void main() {
  getData();
  print('其他事情');
}

void getData() {
  print('开始data=$_data');

  Future(() {
    _data = '网络数据';
    print('结束data=$_data');
  });
}

结果:

flutter: 开始 data=1
flutter: 其他事情
flutter: 结束 data=网络数据

2.Flutter是单线程

import 'dart:io';

String _data = '1';
void main() {
  getData();
  sleep(Duration(seconds: 2)); //睡几秒
  print('其他事情');
}

void getData() {
  print('开始 data=$_data');

  Future(() {
    _data = '网络数据';
    print('结束 data=$_data');
  });
}

增加了sleep方法调用,无论设置几秒钟,执行顺序不变。 打印结果:

flutter: 开始 data=1
flutter: 其他事情
flutter: 结束 data=网络数据

打印结果不会变,只是在‘开始’和‘其他事情’这两个打印之间增加时长,这点可以说明Flutter是单线程的开发模式

3.async和await

规则:

  1. await所在的函数必须必须被async修饰
  2. await必须修饰异步方法
String _data = '1';
void main() {
  getData();
  print('其他事情');
}

//async修饰
void getData() async {
  print('开始 data=$_data');
  //await修饰异步方法Future
  await Future(() {
    for (int i = 0; i < 1000000000; i++) {
      _data = '网络数据';
    }
  });
  print('结束 data=$_data'); 
}

打印结果:

flutter: 开始 data=1
flutter: 其他事情
flutter: 结束 data=网络数据

注意:await的性质和栅栏函数相似(可能底层实现就是栅栏函数),后面代码的执行,要等到await修饰的异步函数执行完才能继续执行。可以在‘结束’print后面多写几份print,看看打印结果。

4.then

如果想要Future后面的部分代码想在Future执行完再执行,而且不影响其他代码,可以是用then来实现

String _data = '1';
void main() {
  getData();
  print('其他事情');
}

void getData() async {
  print('开始 data=$_data');

  Future future = Future(() {
    for (int i = 0; i < 1000000000; i++) {
      _data = '网络数据';
    }
  });

  future.then((value) => print('结束 data=$_data'));

  print('干点其他事情!');
}

打印结果

flutter: 开始 data=1
flutter: 干点其他事情!
flutter: 其他事情
flutter: 结束 data=网络数据

通过打印可以看到‘干点其他事情’的print先执行,而‘结束’print是等待Future后才执行的。

5.then中的value参数

通过future.dart文件中的注释了解到:then是一个回调函数,在future完成时进行的回调。value参数是future中的返回值

String _data = '1';
void main() {
  getData();
  print('其他事情');
}

void getData() async {
  print('开始 data=$_data');

  Future future = Future(() {
    for (int i = 0; i < 1000000000; i++) {
      _data = '网络数据';
    }
    return _data;
  });

  future.then((value) => print(value));

  print('干点其他事情!');
}

打印结果:

flutter: 开始 data=1
flutter: 干点其他事情!
flutter: 其他事情
flutter: 网络数据 //then中打印

6.捕获异常

Future中抛出异常,并且捕获这个异常

String _data = '1';
void main() {
  getData();
  print('其他事情');
}

void getData() async {
  print('开始 data=$_data');

  Future future = Future(() {
    for (int i = 0; i < 1000000000; i++) {
      _data = '网络数据';
    }
    //抛出异常
    throw Exception('网络异常');
  });

  future.catchError((e) {
    //捕获异常
    print('捕获了异常' + e.toString());
  });

  future.then((value) => print(value));

  print('干点其他事情!');
}

打印报错

flutter: 开始 data=1
flutter: 干点其他事情!
flutter: 其他事情
flutter: 捕获了异常Exception: 网络异常
[VERBOSE-2:ui_dart_state.cc(157)] Unhandled Exception: Exception: 网络异常
#0      getData.<anonymous closure> (package:futruedemo/main.dart:14:5)
#1      new Future.<anonymous closure> (dart:async/future.dart:176:37)
#2      _rootRun (dart:async/zone.dart:1180:38)
#3      _CustomZone.run (dart:async/zone.dart:1077:19)
......

异常捕获到了,但是下面有报错。原因是既然捕获了异常,后面的代码就不要执行then操作了。这个地方联想我们平时的开发,其实很好理解

if (有异常) {
    捕获异常
} else {
    执行没有异常的逻辑
}

平时我们都是这么写判断逻辑的,也就是说只能走其中的一个逻辑。所以前面的报错代码的原因,应该和这个类似。

解决方法:then中有可选回调方法{Function onError},通过这个回调函数来实现捕获异常的逻辑

String _data = '1';
void main() {
  getData();
  print('其他事情');
}

void getData() async {
  print('开始 data=$_data');

  Future future = Future(() {
    for (int i = 0; i < 1000000000; i++) {
      _data = '网络数据';
    }
    //抛出异常
    throw Exception('网络异常');
  });

  //注释future捕获异常代码
//  future.catchError((e) {
//    //捕获异常
//    print('捕获了异常' + e.toString());
//  });

  //增加onError回调
  future.then((value) {
    print('then来了');
    print(value);
  }, onError: (e) {
    print('捕获了异常' + e.toString());
  });

  print('干点其他事情!');
}

打印结果:

flutter: 开始 data=1
flutter: 干点其他事情!
flutter: 其他事情
flutter: 捕获了异常Exception: 网络异常

可以看到then中的print没有执行。

链式编程应用

1.应用示例

future的catchError回调作用?其实Dart语法提供了链式编程的方式,本人认为future的catchError就是为了链式编程而提供的方法。 链式编程实现:

String _data = '1';
void main() {
  getData();
  print('其他事情');
}

void getData() async {
  print('开始 data=$_data');

  Future(() {
    for (int i = 0; i < 1000000000; i++) {
      _data = '网络数据';
    }
    //抛出异常
    throw Exception('网络异常');
  }).catchError((e) {
    //捕获异常
    print('捕获了异常' + e.toString());
  }).then((value) {
    print('then来了');
    print(value);
  });

  print('干点其他事情!');
}

打印结果:

flutter: 开始 data=1
flutter: 干点其他事情!
flutter: 其他事情
flutter: 捕获了异常Exception: 网络异常
flutter: then来了
flutter: null

2.注意链式变成的顺序

应该有人注意到then方法的print打印了,这个是因为链式编程是有顺序要求的。修改一下then和catchError的顺序就好了。

...
Future(() {
    for (int i = 0; i < 1000000000; i++) {
      _data = '网络数据';
    }
    //抛出异常
    throw Exception('网络异常');
  }).then((value) {
    print('then来了');
    print(value);
  }).catchError((e) {
    //捕获异常
    print('捕获了异常' + e.toString());
  });
...
flutter: 开始 data=1
flutter: 干点其他事情!
flutter: 其他事情
flutter: 捕获了异常Exception: 网络异常

注意:then方法会执行,只是then中的回调不会执行

3..其他常用的Future可以链式的方法

  1. whenComplete:完成后的回调
  2. timeout:超时回调

这两个在平时开发中使用的频率也很高,使用起来也比较容易,所以就简单记录一下了。

多个Future执行顺序

1.串行队列

多个Future情况,执行顺序类似于串行队列,下面的例子的打印结果可以证明:

void main() {
  testFuture();
  print('其他事情');
}

//多个future
void testFuture() {
  Future(() {
    return '任务1';
  }).then((value) => print('$value结束'));

  Future(() {
    return '任务2';
  }).then((value) => print('$value结束'));

  Future(() {
    return '任务3';
  }).then((value) => print('$value结束'));

  Future(() {
    return '任务4';
  }).then((value) => print('$value结束'));

  print('任务添加完毕');
}

打印结果:

flutter: 任务添加完毕
flutter: 其他事情
flutter: 任务1结束
flutter: 任务2结束
flutter: 任务3结束
flutter: 任务4结束

2.增加sleep方法

在‘任务1’中增加sleep方法,时长1秒:

void testFuture() {
  Future(() {
    sleep(Duration(seconds: 1));
    return '任务1';
  }).then((value) => print('$value结束'));

  Future(() {
    return '任务2';
  }).then((value) => print('$value结束'));

  Future(() {
    return '任务3';
  }).then((value) => print('$value结束'));

  Future(() {
    return '任务4';
  }).then((value) => print('$value结束'));

  print('任务添加完毕');
}

打印结果

flutter: 任务添加完毕
flutter: 其他事情
flutter: 任务1结束
flutter: 任务2结束
flutter: 任务3结束
flutter: 任务4结束

查看结果可知,增加sleep不会影响异步函数的执行顺序,再次证明异步队列是串行模式,进而可以再次证明,Flutter是单线程的。

3.调整任务执行顺序

任务是按照代码顺序执行的,如果想让‘任务1’执行后,执行‘任务4’。最简单的方式时调整代码顺序。但是这样不好,不好的点在于如果代码又臭又长,你很难把其中的逻辑看透,会埋下很多的隐患。

比较好的解决方式,就是利用链式编程的方式,把要执行的‘任务’放到前一个‘任务’的then中处理:例如‘任务4’在‘任务1’之后执行

void testFuture() {
  Future(() {
    sleep(Duration(seconds: 1));
    return '任务1';
  }).then((value) {
    print('$value结束');
    //执行任务4
    return '任务4';
  }).then((value) => print('$value结束'));

  Future(() {
    return '任务2';
  }).then((value) => print('$value结束'));

  Future(() {
    return '任务3';
  }).then((value) => print('$value结束'));
  

  print('任务添加完毕');
}

打印结果:

flutter: 任务添加完毕
flutter: 其他事情
flutter: 任务1结束
flutter: 任务4结束
flutter: 任务2结束
flutter: 任务3结束

这样只要处理你要处理的业务模块就好,不用考虑其他异步任务的业务逻辑。

4.任务分组

两个或者多个任务执行后,执行后续操作代码写法:

void main() {
  testGroupFuture();
  print('其他事情');
}

//分组
void testGroupFuture() {
  Future.wait([
    Future(() {
      return '任务1';
    }),
    Future(() {
      return '任务2';
    })
  ]).then((value) => print(value[0] + value[1]));
}

打印结果:

flutter: 其他事情
flutter: 任务1任务2

使用Future.wait来实现任务分组,then中的value返回的也是一个数组,用下标获取每个任务的返回值。

之前证明了Flutter是单线程执行的,这种方式和用链式的方式(Future.then.then)执行效率上没有区别。在代码的可读性上来说会好一点。

Dart事件循环队列

Dart中有两种队列:事件队列和微任务队列

  1. 事件队列(event queue):比如我们上面用示例中用到的Futur异步任务加入的队列。
  2. 微任务队列(microtask queue):它的优先级高于事件队列,只有微任务队列中有微任务存在,就会一直霸占着资源,事件队列只能等待直到微任务队列中所有微任务执行结束。

事件队列

我们平时使用比较多的Future,将异步任务放到优先级比较低的事件队列中,执行逻辑比较好理解:

  1. 声明一个Future时,Dart将异步任务函数体放到事件队列(event queue)中,然后立即返回,后续代码继续同步执行。
  2. 后继代码同步执行完毕,按照事件队列的加入顺序,依次取出执行代码块。在代码块中继续按照同步执行。

微任务队列

代码示例

void testFuture4() {
  Future x = Future(() => print('a'));
  Future(() => print('b'));
  scheduleMicrotask(() => print('c'));
  x.then((value) => print('d'));
  print('e');
}

代码中scheduleMicrotask是调度微任务队列,并把print('c')放入到微任务队列中,看看结果打印:

flutter: e
flutter: c
flutter: a
flutter: d
flutter: b

代码解析

  1. print('a')放到事件队列中,并用变量x接收Future()返回值;
  2. print('b')放到事件队列中;
  3. print('c')放到微任务队列中,该代码块执行优先于1、2的代码块;
  4. x.then中的print('d')依赖第1步执行之后,才能执行;
  5. print('e')是同步代码,优先执行,所以先打印‘e’;
  6. 同步代码执行完毕,开始查看微任务队列,打印‘c’;
  7. 微任务队列为空,开始执行事件队列,按照加入的顺序,先执行打印‘a’,然后执行then方法,打印‘d’;
  8. 最后打印‘b’;

then的再次分析

紧接着Future中回调结束后,会执行then的回调方法。那么then的回调是在事件队列中,还是微任务队列中呢?

猜测1:事件队列中

我们还用上面例子中的代码,这次把scheduleMicrotask和x.then代码互换一下位置:(为了下面说明方便,这里把打印字母换成打印数字,注意,3和4对应的代码c和d,互换了顺序)

void testFuture4() {
  Future x = Future(() => print('1'));
  Future(() => print('2'));
  //调整代码部分
  x.then((value) => print('4'));
  scheduleMicrotask(() => print('3'));
  //调整代码部分
  print('5');
}

执行结果:

flutter: 5
flutter: 3
flutter: 1
flutter: 4
flutter: 2

如果是事件队列中,那么执行顺序应该是5-3-1-2-4,但显然和执行打印结果不一样。所以说在事件队列中这个不成立。

猜测2:微任务队列中

本人开始认为是在微任务队列中,then方法的定义中有段注释:

大概意思:如果当前future已经完成,callback不会立即调用,而是将要在稍后的微任务中被安排调用。

上面例子代码:同步代码打印5,微任务打印3,执行事件队列中的打印1之后将打印4放到微任务中,然后执行微任务中的打印4,最后打印2。很完美的解释通了。

官方的注释+示例代码,这样可以证明then是将回调任务放在微任务队列中。

其实也不在微任务队列中

但当我遇到下面的代码时,整个人就凌乱了:

void main() {
  Future x = Future(() {
    print('1');
    scheduleMicrotask(() => print('2'));
    return '1结束';
  });
  x.then((value) => print(value + '3'));
  print('4');
}

代码分析:(注意:这是不正确的

  • 打印4,同步执行
  • 开始异步执行Future,打印1,将print('2')加入到微任务队列中,Future执行完毕。
  • 此时微任务队列中不为空,执行打印2
  • 最后执行then中的回调‘1结束3’
  • 打印顺序:4-1-2-1结束3

看打印结果:

哈哈,是不是凌乱了!数据结构中,队列肯定是一头进一头出,不会有插队的情况。那么唯一可以解释的就是future和then是一体的。也就是future的回调执行完,不代表这个任务执行完,等到then中的回调执行完才是当前在事件队列中的任务执行完成。

可能有人会问注释中怎么解释,这个也是我之前一直任务在微任务队列中的主要原因,但无法解释上述代码,所以我个人偏向于注释作者偷懒的概率较大。

此处是以目前个人对Dart学习而总结出的结论,因为网上也没有找到具体官方解释,所以我只能用我当前看到的现象进行总结。欢迎看到文章此处的同学一起讨论一下。