Flutter —— 异步编程

这是我参与11月更文挑战的第20天,活动详情查看:2021最后一次更文挑战

1. 异步编程

1.1 Future

Future 类,其表示一个 T 类型的异步操作结果。如果异步操作不需要结果,则类型为 Future。也就是说首先Future是个泛型类,可以指定类型。如果没有指定相应类型的话,则Future会在执行动态的推导类型。 在网络请求中经常使用异步编程,那么来探索一下Flutter 中的异步编程。

dart是单线程的,所以底层没有锁之类的东西,但是这不代表着dart不能异步,异步不代表多线程。 下面代码运行后发现做其他事情被堵塞住了,这里async不起作用是因为需要搭配Future使用。

在这里插入图片描述

将耗时操作使用Future包装起来,这里可以看到做其他事情就不会被堵塞住了,那么现在即使方法不加async也是异步的,因为Future里面已经是异步的了。或者说async不会异步执行,Future才会异步执行。

在这里插入图片描述

如果把 print('结束data = $_data'); 放在Future外面那么其就会先于Future里面的代码执行。

在这里插入图片描述

1.2 async ,await

那么如何让等待Future里面的执行完在执行后面代码呢?这时候需要用到await。加了await之后,后面的代码就会等待Future里面的执行完之后在执行。这里的await需要搭配async使用,而没有await的情况下,async是没有意义的。await后面的操作也需要是异步的。那么也就是说,Future是用来异步执行的,而async和await搭配是用来让Future里面的某块代码同步执行的。

在这里插入图片描述

1.3 Future.then()

Future的所有API的返回值仍然是一个Future对象,所以可以很方便的进行链式调用。这里可以使用then来将特定的需要等待的任务放进去,然后不堵塞后面的任务的执行。 如果then没有返回值的话,那么value就是null。

在这里插入图片描述

这里看到Future里面return的话那么value就是return的那个值。这个时候,Future里面返回的数据会被Future包装,然后给到了then里面 的value。

在这里插入图片描述

1.4 Future.catchError

异步中的错误是用catchError来进行处理的。当异步任务中出现异常之后, 如果调用下列方法就会报错,这是因为任务里面抛出了异常没有处理。

getData() async {
  print('开始data = $_data');
  //耗时操作
  Future future =  Future(() {
    for (int i = 0; i < 10000000; i++) {}
    throw Exception('网络异常');
    return "假的网络数据";
  });
  future.then((value) {
    print('value = $value');
  });

  print('做一点其他事情');
}

复制代码

在.then 下面添加代码

  future.catchError((e){print("捕获到了错误:$e");}); 
复制代码

运行后发现还是报错,但是捕获到了异常。正常来说如果处理了异常,就不应该报错了,那么这里是执行顺序的问题吗?

在这里插入图片描述

将catch和then交换位置后运行,发现还是报错

在这里插入图片描述

把then注释掉后发现不报错了。

在这里插入图片描述

那么这里要怎么处理呢?这里一般用链式调用。

getData() async {
  print('开始data = $_data');
  //耗时操作
  Future future = Future(() {
    for (int i = 0; i < 10000000; i++) {}
    throw Exception('网络异常');
    return "假的网络数据";
  }).then((value) {
    print('value = $value');
  }).catchError((e) {
    print("捕获到了错误:$e");
  });

  print('做一点其他事情');
}
复制代码

或者在.then中添加onError对错误进行处理 在这里插入图片描述

 future.then((value) {
    print('value = $value');
  },onError: (error){print("捕获到了错误:$error");});
复制代码

onError是在.then这一次对错误进行处理,而catchError则是多次链式调用中的错误处理 如果在.then之前catchError了,那么.then中的value就是catchError中传过来的值了。在catchError之前的.then不会执行。 在这里插入图片描述 在CatchError之后添加了whenComplete运行后发现还是报出了异常。

getData() async {
  print('开始data = $_data');
  //耗时操作
  Future future = Future(() {
    for (int i = 0; i < 10000000; i++) {}
    throw Exception('网络异常');
    return "假的网络数据";
  });

      future.catchError((e) {
    print('error = $e');
    return "错误";
  }).then((value) => print('$value'));

  future.whenComplete(() => print("完成了"));

  print('做一点其他事情');
}
复制代码

这个时候可以在whenComplete之后添加一个catchError,或者使用链式调用。

    future.catchError((e) {
    print('error = $e');
    return "错误";
  }).then((value) => print('$value')).whenComplete(() => print("完成了"));
复制代码

上面的例子可以看到链式调用能避免大部分的错误,所以在一般都是使用链式调用来进行处理的,并且把catchError放到最后调用,这样出现异常的时候,前面的.then就不会执行了。如果链式太长的话,可以创建方法来让链式更加简洁。

getData() async {
  print('开始data = $_data');
  //耗时操作
  Future future = Future(() {
    for (int i = 0; i < 10000000; i++) {}
    throw Exception('网络异常');
    return "假的网络数据";
  }).then(thenFunc).whenComplete(completeFunc).catchError((e) {
    print('error = $e');
    return "错误";
  });

  print('做一点其他事情');
}

FutureOr thenFunc(String value) {
}

FutureOr<void> completeFunc() {
}

复制代码

1.5 多个Future

Future是放在队列中的,所以Future的执行是有顺序的。下面的代码结果输出是什么呢?

void main() {
  testFuture();
  print("A");
}

void testFuture() {
   Future((){
    sleep(Duration(seconds: 2));
    print("C");
  });

  print("B");
}
复制代码

运行后看到是B->A->C; 在这里插入图片描述 那么下面的代码运行后会打印什么呢?

void main() {
  testFuture();
  print("A");
}

void testFuture() async {
  await Future((){
    sleep(Duration(seconds: 2));
    print("C");
  }).then((value) =>   print("D"));

  print("B");
}
复制代码

运行后发现是A->C->D->B,这里因为B被C堵塞了,所以A会先执行,然后C执行完之后执行D,最后执行B。 在这里插入图片描述 那么下面代码的输出结果是什么呢?

void main() {
  testFuture();
  print("A");
}

void testFuture() async {
   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("任务添加完毕");
}

复制代码

运行后发现是任务添加完毕->A->任务1结束->任务2结束->任务3结束->任务4结束。 在这里插入图片描述

那么这里任务一定是按顺序执行的吗?在任务二添加sleep后重新执行,发现任务顺序还是一样的。 在这里插入图片描述 这说明这里会按异步任务的添加顺序执行的。

1.6 Future.wait

当需要等待多个Future完成,并收集它们的结果,可以使用Future.wait。这个时候.then就会等wait里面所有任务完成后在执行,然后返回的值可以用数组来取。wait里面的任务同时处理,但是是按添加顺序执行的,而如果是链式执行的话,则是一个执行完在执行下一个。

 Future.wait([
  Future((){
    return "任务1";
  }),
  Future((){
  return "任务2";
  }),
  Future((){
  return "任务3";
  }),
  Future((){
  return "任务4";
  }),
  ]).then((value) => print(value[0] + value[1] + value[2]   ));
复制代码

1.7 microtask

Dart的事件循环(event loop)

在Dart中,实际上有两种队列:

  • 事件队列(event queue),包含所有的外来事件:I/O、mouse events、drawing events、timers、isolate之间的信息传递。
  • 微任务队列(microtask queue),表示一个短时间内就会完成的异步任务。它的优先级最高,高于event queue,只要队列中还有任务,就可以一直霸占着事件循环。microtask queue添加的任务主要是由 Dart内部产生。

因为 microtask queue 的优先级高于 event queue ,所以如果 microtask queue有太多的微任务, 那么就可能会霸占住当前的event loop。从而对event queue中的触摸、绘制等外部事件造成阻塞卡顿。

在每一次事件循环中,Dart总是先去第一个microtask queue中查询是否有可执行的任务,如果没有,才会处理后续的event queue的流程。 在这里插入图片描述

异步任务我们用的最多的还是优先级更低的 event queue。Dart为 event queue 的任务建立提供了一层封装,就是我们在Dart中经常用到的Future。

正常情况下,一个 Future 异步任务的执行是相对简单的:

声明一个 Future 时,Dart 会将异步任务的函数执行体放入event queue,然后立即返回,后续的代码继续同步执行。 当同步执行的代码执行完毕后,event queue会按照加入event queue的顺序(即声明顺序),依次取出事件,最后同步执行 Future 的函数体及后续的操作。 上面说过了microtask队列的优先级比较高,那么使用microtask就可以让任务优先执行。 下面代码的先执行代码1,代码2,A以及B。

void testFuture3() {
  print('外部代码1');
  Future(()=>print('A')).then((value) => print('A结束'));
  Future(()=>print('B')).then((value) => print('B结束'));
  print('外部代码2');
}
复制代码

添加了微代码后,那么微任务就会在异步任务之前执行。

void testFuture3() {
  print('外部代码1');

  Future(()=>print('A')).then((value) => print('A结束'));
  Future(()=>print('B')).then((value) => print('B结束'));
  scheduleMicrotask(() {
    print("微任务A");
  });
  print('外部代码2');
}

复制代码

下面的任务执行后打印情况是什么样的呢?这里5一定是先执行的,然后执行微任务3,然后异步任务按照添加的顺序执行,那么就会先执行future1打印1和4,最后执行future2 打印2,所以打印 5 —— 3 —— 1 —— 4 —— 2;

void testFuture4() {
 Future future1 =  Future((){print('1');});
 Future future2 =  Future((){print('2');});
 scheduleMicrotask(() {
   print("微任务3");
 });
 future1.then((value) => print('4'));
 print('5');
}
复制代码

运行后验证果真是的。

在这里插入图片描述

那么这里的打印顺序是什么呢?这里future3最先被添加到队列,所以依然会比 1, 2 优先执行,所以会先打印6,所以打印 5 —— 3 —— 6 —— 1 —— 4 —— 2;

在这里插入图片描述

打印结果:

在这里插入图片描述

那么如果是这样的话,打印结果会是什么呢?按照图片里的,执行future3的时候会把自身任务执行完再重新开始循环,那么也就是说,这里5,3之后会先打印6,8 ,然后再打印7,然后再打印142。

void testFuture4() {
  Future future3 = Future(() => null);
  future3.then((value) {
    print('6');
    scheduleMicrotask(() {
      print("7");
    });
  }).then((value) =>  print("8"));

  Future future1 = Future(() {
    print('1');
  });

  future1.then((value) => print('4'));
  Future future2 = Future(() {
    print('2');
  });
  scheduleMicrotask(() {
    print("3");
  });

  print('5');
}
复制代码

打印结果:

在这里插入图片描述

那么把.then拆出来的话是什么结果呢?其实这里相当于把.then里面的任务放到微任务里面去了,所以8依然会优先执行。这也是为什么 future1里面的then的任务会比 future2先执行。

  Future future4 =  future3.then((value) {
    print('6');
    scheduleMicrotask(() {
      print("7");
    });
  });
  
  future4.then((value) => print("8"));
复制代码

打印结果:

在这里插入图片描述

2. 总结

  • Dart中的异步
    • Future对象来完成异步操作。
      • 通过工厂构造方法创建Future对象。
      • 参数为Dart的函数
        • 函数的执行代码将被放入事件队列异步执行。
    • async 和 await 。如果Future内部代码希望同步执行,则使用await修饰。被async修饰的函数为异步执行。
    • Future结果处理
      • Future.then 用来注册一个Future完成时要调用的回调
      • Future.catchError注册一个回调,来捕捉Future的error
        • Future.catchError回调只处理原始Future抛出的错误,不能处理回调函数抛出的错误
        • onError只能处理当前Future的错误
      • Future.whenComplete 在Future完成之后总是会调用,不管是错误导致的完成还是正常执行完毕。
  • Dart的事件循环
    • 在Dart中,实际上有两种队列:
      • 事件队列(event queue),包含所有的外来事件:I/O、mouse events、drawing events、timers、isolate之间的信息传递。
      • 微任务队列(microtask queue),表示一个短时间内就会完成的异步任务。它的优先级最高,高于event queue,只要队列中还有任务,就可以一直霸占着事件循环。microtask queue添加的任务主要是由 Dart内部产生。

Guess you like

Origin juejin.im/post/7032581490223366151