likes
comments
collection
share

【Flutter基础】Dart中的异步 Future

作者站长头像
站长
· 阅读数 16

前言

上一篇文章《【Flutter基础】Dart中的并发Isolate》中,我们介绍了 Isolate 的相关知识点。今天我们就回过头来看一看 FutureFuture 可以说是我们在项目中使用最多的异步调用对象,但我们真的完全了解 Future 吗?看完这篇文章,我想你能够找到答案~

项目源码地址 Github

一、Future的基本用法

1.1 Future(FutureOr computation())

这里有一个计数器的例子,通过 Future(FutureOr<T> computation()) 来实现异步计数功能,代码如下:

/// 1.1 Future(FutureOr<T> computation())
void futureTest() {
  int _count = 0;
  print('1.开始count=$_count');
  Future(() {
    for (int i = 0; i < 10000000; i++) {
      _count++;
    }
    print('2.计算完成count=$_count');
  });
  print('3.结束count=$_count');
}

运行结果如下:

【Flutter基础】Dart中的异步 Future

可以看到,Future 中的执行函数,会在最后执行打印,且不会阻塞后续的代码执行。从而导致打印结果为:1-3-2,而不是 1-2-3

1.2 Future.value()

Future.value() 方法非常简单,直接返回一个 Future 的返回值:

/// 1.2 Future.value
void futureValueTest() {
  Future<int>.value(2021).then((value) {
    print('value: $value');
  });
}

运行结果:

【Flutter基础】Dart中的异步 Future

1.3 Future.then

为了解决上述打印顺序的问题,这里我们使用 Future.then 方法来实现代码的顺序加载。代码如下:

/// 1.3 Future.then
void futureThenTest() {
  int _count = 0;
  print('1.开始count=$_count');
  Future(() {
    for (int i = 0; i < 10000000; i++) {
      _count++;
    }
    print('2.计算完成count=$_count');
  }).then((value) => print('3.结束count=$_count'));
}

执行结果:

【Flutter基础】Dart中的异步 Future

可以看到此时的打印顺序为 1-2-3 并且 3 获取到的值确实是计算后的值,这样就实现了异步调用的同步操作。

1.4 Future.delayed

通过 Future.delayed 我们能够实现延时调用功能,代码如下:

/// 1.4 Future.delayed
void futureDelayedTest() {
  print('1.开始执行: ${DateTime.now()}');
  Future.delayed(const Duration(seconds: 2), () {
    print('2.延时2秒执行: ${DateTime.now()}');
  });
  print('3.结束执行: ${DateTime.now()}');
}

我们通过 delayed 延迟2秒执行计算,运行结果:

【Flutter基础】Dart中的异步 Future

可以看到确实延迟了 2秒 执行计算任务。

1.5 await-async

除了使用 Future.then 方法来实现同步功能外,我们还可以使用 await-async 关键字来实现异步调用的同步功能。代码如下:

/// 1.5 async 和 await
Future<void> awaitAsyncTest() async {
  int _count = 0;
  print('1.开始count=$_count');
  await Future(() {
    for (int i = 0; i < 10000000; i++) {
      _count++;
    }
    print('2.计算完成count=$_count');
  });
  print('3.结束count=$_count');
}

运行结果:

【Flutter基础】Dart中的异步 Future

不知道大家发现了没有,对比 futureValueTest()awaitAsyncTest() 方法的使用,我们仅仅添加了 await-async 关键字就实现了异步转同步的操作,无需使用函数回调等嵌套回调处理,这让我们写的代码更加清晰明确,除此之外在某些其他方面 await-async 也是更有优势。

Future.thenawait-async 调用的区别在于,Future.then 无需给方法添加 async关键字,缺点在于返回值会嵌套到 Future.then 中,导致方法获取返回值更加麻烦。这里推荐使用 await-async 更加直观。

二、Future的高级用法

2.1 异常处理

2.1.1 catchError

我们可以通过 catchError 来捕获函数调用中抛出的异常,这里我们通过 throw Exception() 手动抛出错误。代码如下:

/// 2.1.1 catchError
void catchErrorTest() {
  int _count = 0;
  print('1.开始count=$_count');
  Future(() {
    _count++;
    throw Exception('计算错误');
    print('2.计算完成count=$_count');
  })
      .then((value) => print('3.计算完成count=$_count'))
      .catchError((error) => print('4.捕获异常 : $error'))
      .then((value) => print('5.计算完成count=$_count'));
}

运行结果:

【Flutter基础】Dart中的异步 Future

分析输出结果,我们可以发现以下几点结论:

  1. throw Exception 异常抛出时,在捕获前,后面的代码都不会再执行,所以 2、3 处没有打印。
  2. 异常捕获后,可以通过 catchError 获取异常 error 信息,执行异常处理。
  3. 异常捕获后,后续代码仍然可以执行。
2.1.2 onError

除了 catchError 可以捕获异常外,我们还可以通过 Future.thenonError 方法捕获异常。代码如下:

/// 2.1.2 onError
void onErrorTest() {
  int _count = 0;
  print('1.开始count=$_count');
  Future(() {
    _count++;
    throw Exception('计算错误1');
  }).then((value) {
    print('2.计算完成count=$_count');
  }, onError: (error) {
    print('3.捕获异常 : $error');
  });
}

运行结果:

【Flutter基础】Dart中的异步 Future

可以看到,当抛出异常时,Future.thenonValue 函数不会执行,而是在 onError 函数中捕获异常。

2.1.3 catchError 与 onError 的差异

为了对比 catchError 与 onError的差异,我们这里有一个 onError 的案例:

/// 2.1.3 catchError 与 onError 的差异
void futureErrorTest1() {
  int _count = 0;
  print('1.开始count=$_count');
  Future(() {
    _count++;
    throw Exception('计算错误1'); // tag1
  }).then((value) {
  }, onError: (error) {
    print('2.捕获异常 : $error'); // tag2
    // throw Exception('计算错误2');
    throw error;
  }).then((value) => null, onError: (error) {
    print('3.捕获异常 : $error'); // tag3
  });
}

我们在 tag1 处抛出一个异常,在 tag2 处理进行捕获并继续抛出该异常,最后在 tag3 处理继续捕获异常。

运行结果:

【Flutter基础】Dart中的异步 Future

可以看到,onError 不仅可以捕获事件源抛出的异常,也可以捕获后续执行函数抛出的异常。

我们再来看一下 catchError 的调用,代码如下:

/// 2.1.3 catchError 与 onError
void futureErrorTest2() {
  int _count = 0;
  print('1.开始count=$_count');
  Future(() {
    _count++;
    throw Exception('计算错误1');
  }).catchError((error) {
    print('2.捕获异常 : $error');
    throw Exception('计算错误2');
    // throw error;
  }).catchError((error) {
    print('4.捕获异常 : $error');
  });
}

运行结果:

【Flutter基础】Dart中的异步 Future

可以看到,虽然 catchError 也能够正常捕获到异常,但是最后会抛出一个 Unhandled exception 的异常 出去。分析抛出的异常原因,是由于 catchError 回调必须返回一个 Future's 类型导致的。但是我们的Future本身没有返回值,那么该如何处理呢?

处理方式:

  1. 建议使用 async-awaittry-catch 而不是使用.catchError,这样可以避免这种混淆(我们上面介绍其他方面的优势在这里也可以体现)。

  2. 如果一定要使用 catchError 可以在事件源上添加一个 then 函数回调,此时会将原来的返回值由 Future<Never> 类型转换成 Future<Null> 类型,从而可以为 catchError 提供一个 Future 类型的返回值,如此即可避免该问题发生。

    // 2.1.3  catchError 与 onError
    Future<void> futureErrorTest3() async {
      int _count = 0;
      print('1.开始count=$_count');
      Future(() {
        _count++;
        throw Exception('计算错误1');
      }).then((value) {
      }).catchError((error) {
        print('2.捕获异常 : $error');
      });
    }
    

    运行结果:

    【Flutter基础】Dart中的异步 Future

2.1.4 Future.error

除了上述通过 throw Exception 抛出异常外,我们还是通过 Future.error 来抛出一个异常。代码如下:

/// 2.1.4 Future.error
futureErrorTest(){
  int _count = 0;
  print('1.开始count=$_count');
  Future(() {
    _count++;
    return Future.error(Exception('计算错误1'));
  }).catchError((error) {
    print('2.捕获异常 : $error');
  });
}

运行结果:

【Flutter基础】Dart中的异步 Future

这里我们发现通过 Future.error 抛出的异常,并不会导致 catchError 报错。因此对于 catchError 的处理还有一种方式,那就是将 Exception 通过 Future.error 来转换。

2.2 Future.whenComplete

FutureFuture.whenComplete 表示在整个 Future 链路执行完成后最终的调用。即使在 Future 链路调用中发生了异常,该方法也一定会执行。一般我们用来做一些 IO操作 的结束处理,比如:读写文件流的 close、数据库cursor 的关闭等。

/// 2.2 Future.whenComplete
void whenCompleteTest() {
  Future(() {
    throw Exception('计算错误1');
  }).then((value){
  }).catchError((error){
    print('捕获异常: $error');
    throw error;
  }).whenComplete(() {
    print('whenComplete');
  });
}

运行结果:

【Flutter基础】Dart中的异步 Future

2.3 Future.wait

不知道大家在工作中有没有遇到过这样的需求,某一个页面需要 2个接口的数据来拼接显示。此时,一般我们想到的是采用 await-async 来实现两个接口的数据同步处理,但这并不是最优解,因为两个接口的请求是串行的。我们希望的是两个接口能够同时发起请求,当两个接口请求都完成时即可进行回调。此时 Future.wait 就排上了用场。

Future.wait 表示同时执行多个异步任务,在所有任务执行完成,或者发生异常时进行回调。

// 2.3 Future.wait
void futureTest11() {
  Future.wait([
    Future(() => print('任务1')),
    Future(() => print('任务2')),
    Future(() => print('任务3')),
  ]).then((value) => print('完成所有任务'));
}

运行结果:

【Flutter基础】Dart中的异步 Future

2.4 Future.timeout

Future 中 Future.timeout 方法表示给异步任务设置超时时长并在任务超时后进行回调处理

/// 2.4 Future.timeout
void futureTest12() {
  Future(() {
    return Future.delayed(const Duration(seconds: 3), () => print('1.完成任务'));
  }).timeout(const Duration(seconds: 2), onTimeout: () {
    print('2.任务超时');
  }).then((value) {
    print('3.结束任务');
  });
}

运行结果:

【Flutter基础】Dart中的异步 Future

2.5 Future.doWhile

Future.doWhile 顾名思义在 Future中执行 do-while 处理。我们也可以直接在 Future 里面执行 do-while 循环,这里只不过封装了一层而已。代码如下:

/// 2.5 Future.doWhile
void doWhileTest() {
  int _count = 0;
  Future.doWhile(() async {
    _count++;
    await Future.delayed(const Duration(seconds: 1));
    if (_count == 3) {
      print('Finished with $_count');
      return false;
    }
    return true;
  });
}

运行结果:

【Flutter基础】Dart中的异步 Future

2.6 Future.forEach

Future.forEach 也和我们日常用到的 forEach 一样,遍历每个item处理,遍历结束后,返回执行结果。

这里我们看一下 Future.forEach 源码,发现其内部是通过 Future.doWhile 来实现迭代器的遍历。

  static Future<void> forEach<T>(
      Iterable<T> elements, FutureOr action(T element)) {
    var iterator = elements.iterator;
    return doWhile(() {
      if (!iterator.moveNext()) return false;
      var result = action(iterator.current);
      if (result is Future) return result.then(_kTrue);
      return true;
    });
  }

2.7 Future.microtask

Future.microtask 表示将一个 Future 添加到微任务队列中执行,执行结束后回调到Future中。对于微任务队列大家可能比较陌生,更好奇为什么要这样处理,对于这些我们先按下不讲。后面介绍 Dart事件循环和队列 时我们在了解。源码:

factory Future.microtask(FutureOr<T> computation()) {
  _Future<T> result = new _Future<T>();
  scheduleMicrotask(() {
    try {
      result._complete(computation());
    } catch (e, s) {
      _completeWithErrorCallback(result, e, s);
    }
  });
  return result;
}

2.8 Future.sync

Future.sync 顾名思义即Future同步的功能。下面是其实现源码:

  factory Future.sync(FutureOr<T> computation()) {
      var result = computation();
      if (result is Future<T>) {
        return result;
      } else {
        // TODO(40014): Remove cast when type promotion works.
        return new _Future<T>.value(result as dynamic);
      }
  }

可以看到先执行 computation() 函数调用,若函数返回值也是 Future 则直接返回,否则通过 _Future.value() 进行包装。

看到这里大家可能有点懵逼,但 new _Future.value 和 Future.value 有啥区别?

这里有一个用例,能够让大家认知 Future.syncFuture.value 的区别。

/// 2.6 Future.sync
Future<void> syncTest() async {
  Future future1 =  Future.value(1); // tag1
  Future future2 = Future<int>(() => 2);	// tag5
  Future future3 = Future.value(Future(() => 3)); // tag6
  Future future4 = Future.sync(() => 4);	// tag4
  Future future5 = Future.sync(() => Future.value(5)); // tag2
  scheduleMicrotask(() => print(6));  // tag3
  future1.then(print);
  future2.then(print);
  future3.then(print);
  future4.then(print);
  future5.then(print);
}

输出结果:

【Flutter基础】Dart中的异步 Future

分析上述结果:

  1. Future.vaue 的优先级是最高的,无论 Future.value 放在何处位置,都会优先执行完,因为它是同步的。
  2. Future.sync 对于 computation 执行结果为 Future<T> 的会直接返回,等价于执行 Future.vaue(4) ,所以 tag2 是第二个打印。
  3. tag3 处是将一个微任务添加到微任务队列中,在当前任务执行结束后,会优先进入到微任务队列的执行,所以 tag3 处第三个打印。
  4. Future.aync 对于 computation 结果为非 Future 类型的值,进行 _Future<T>.value 包装。其优先级在事件队列之前、微任务队列之后,因此 tag4会打印。
  5. 最后执行事件队列中的任务,tag5tag6 会按添加顺序打印。

三、Dart事件循环和队列

3.1 Dart事件循环和队列

Dart 应用程序有一个带有两个队列的事件循环*——* 事件队列微任务队列

如下图所示,当main()执行时,事件循环开始工作。首先,它以 先进先出 顺序执行任何微任务。然后它出列并处理事件队列中的第一项。然后重复这个循环:执行所有微任务,然后处理事件队列中的下一个项目。

【Flutter基础】Dart中的异步 Future

一般我们的代码都是在事件队列里面运行。这也是为什么代码执行出错后,程序缺没有崩溃,仍然可以执行其他代码的原因。

如何添加任务:

  1. Future类,将一个任务添加到事件队列的末尾。
  2. scheduleMicrotask()函数,将一个任务添加到微任务队列的末尾。

当我们使用Future()Future.delayed()时,即在事件队列末尾添加了一个新的任务。但也只是添加了一个任务,并不会立即执行,而是会等事件队列里的任务全部执行完成后,才会执行。这也是 [1.1小节](#1.1 Future(FutureOr computation()) 中执行结果不是按顺序1-2-3 打印的原因。

3.2 Future执行顺序

了解到了Dart的事件循环和队列机制,我们来测试下 Future 任务的执行顺序来帮助大家加深理解。

这里按照从上到下有1-9个编号打印,请大家思考下打印结果是怎样的顺序:

/// 3.2 event loop
void futureEventLoopTest() {
  Future future1 = Future(() { 
    print('任务1');
  });
  future1.then((value) {
    print('任务2');
    scheduleMicrotask(() => print('任务3'));
  }).then((value) {
    print('任务4');
  });

  Future future2 = Future(() => print('任务5'));
  Future(() => print('任务6'));
  scheduleMicrotask(() => print('任务7'));
  future2.then((value) => print('任务8'));
  print('任务9');
}

我们来分析下上述的任务执行过程:

首先,我们可以分析出前面的代码一直是在事件队列和微任务队列添加任务,所以最后一行 任务9 的打印。

当前的任务已经执行完成,开始进入到微任务队列执行,而任务7所在的任务是微任务,开始执行打印。

然后执行 future1,此时按顺序打印任务1任务2任务4并把一个任务添加到微任务队列,执行结束。

然后,进入到微任务队列,执行任务3的打印。

再进入到 future2 的执行,打印任务5任务8,执行结束。

最后,一个 Future 执行,打印任务6

所以执行结果为:9-7-1-2-4-3-5-8-6

【Flutter基础】Dart中的异步 Future

结语

至此,我们完成了对 Future 概念和用法的分析,不知道大家是否全部掌握呢?可以评论区留言。

如果觉得这篇文章对你有所帮助的话,不要忘记一键三连哦,大家的点赞是我更新的动力🥰。

项目源码:Github

参考资料:

Dart官网

事件循环和队列