Flutter中的异步编程——Future

Dart是一个在单线程中运行的程序,这意味着:如果程序在执行中遇到一个需要长时间的执行的操作,程序将会被冻结。为了避免造成程序的冻结,可以使用异步操作使程序在等待一个耗时操作完成时继续处理其他工作。在Dart中,可以使用Future对象来表示异步操作的结果。

Dart的消息循环机制

在进入正题之前,我们先看一下Dart的消息循环机制:


简单总结一下,详细内容可以看文章The Event Loop and Dart

Dart中事件循环的一些主要概念:

  • Dart从两个队列执行任务:event事件队列和microtask微任务队列;
  • Dart的方法是不会被其他Dart代码打断的,当main执行完成后,main isolate的线程就会去逐一处理消息队列中的消息
  • 事件队列具有来自Dart(Future,Timer,Isolate Message等)和系统(用户输入,I/O等);
  • 微任务队列目前仅包含来自Dart;
  • 事件循环会优先处理微任务队列,microtask清空之后才将event事件队列中的下一个项目出队并处理。
  • 一旦两个队列都为空,则应用程序已完成工作,并且(取决于其嵌入程序)可以退出。
  • main()函数以及微任务和事件队列中的所有项目都在Dart应用程序的main isolate上运行。

什么是Future

Future<T>表示一个指定类型的异步操作结果(不需要结果可以使用Future<void>)当一个返回 future 对象的函数被调用时:

  1. 讲函数放入队列等待执行并返回一个未完成的Future对象

  2. 当函数操作执行完成,Future对象变为完成并携带一个值或一个错误
    上面两条分别对应两个状态:

  3. 运行状态(pending),表示任务还未完成,也没有返回值

  4. 完成状态(completed),表示任务已经结束(无论失败还是成功)

例如:

# demo1
main() {
  Future f1 = new Future(() {
    print("我是第一个");
  });
  f1.then((_) => print("f1 then"));
  print("我是main");
}
# print:
# 我是main
# 我是第一个
# f3 then

观察程序输出,首先执行完main函数然后再去执行任务栈中的内容,在该例中也就是我们使用Future假如到event任务栈中的任务<u>then中的方法会在Future处于完成态(completed)时立马执行</u>,之后我们再详细讲解。
Dart提供了数种创建Future的方法,其中最基本的为:

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

demo1中所使用的就是这种方式创建的Future。
其他创建Future的方式包括:

  • Future.value():返回一个指定值的Future
  • Future.delayed():返回一个延时执行的Future
main() {
  Future.delayed(Duration(milliseconds: 200),(){
    print("我是延迟的Future");
  });
  var future = Future.value("我是Future");
  future.then((value) => print(value));
}
# print:
# 我是Future
# 我是延迟的Future

这端代码执行了两个分支:

  • main()方法
  • event队列

Future中的任务调度

前面讲过:当Future执行完成后,then()注册的回调函数会立即执行,但是then中的函数并不会被添加到事件队列中,只是在事件队列中的任务被执行完成后才被立刻执行(可以理解为:将网络请求放在队列中进行执行,拿到结果后在then中刷新UI)。

main() {
  Future f1 = new Future(() => null);
  Future f2 = new Future(() => null);
  Future f3 = new Future(() => {print("创建f3")});
  f3.then((value) => print("我是f3"));
  f2.then((value) => print("我是f2"));
  f1.then((value) => print("我是f1"));
}

上面程序的输出结果为:

我是f1
我是f2
创建f3
我是f3

首先,任务栈符合以FIFO的方式运行,f1,f2,f3一次被加入到任务栈,then()注册的函数并不会被添加到队列,也不会直接运行。当任务栈中任务被执行后,立刻运行then中的函数,依次类推。可以看到,then中的回调函数执行的顺序并不取决于注册的顺序,而仅仅与其Future被加入到任务栈的顺序有关。
注意:new Future(() => null)和new Future(null)有本质上的区别,一个函数体为空,什么都不做;一个是参数为空,不存在函数。
稍微修改一下上例中的代码:

main() {
  Future f1 = new Future(() => null);
  Future f2 = new Future(() => null);
  Future f3 = new Future(() => {print("创建f3")});
  f3.then((_) => print("我是f3"));

  f2.then((_) {
    print("我是f2");
    new Future(() => print("我是一个新的"));
    f1.then((_) {
      print("我是f1");
    });
  }).then((value) => print("我还是f2"));
}

执行结果为:

我是f2
我还是f2
我是f1
创建f3
我是f3
我是一个新的

先看一下then的定义:

Future<R> then<R>(FutureOr<R> onValue(T value), {Function? onError});

这里涉及到两个关键点:

  • 如果Future在then被调用之前已经完成,那么then中的函数会被作为任务添加到microtask队列中
  • then会返回新的新的Future,并且该Future在onValue(then中注册的回调函数)或者onError被执行时就已经处于完成状态了。
  • 如果onValue(回调函数)返回值为一个Future,那么then返回的Future将会在onValue返回的future执行完成后处于完成状态
    关于后面两点:
main() async {
  Future f2 = new Future(() => null);

  f2.then((_) {
        print("我是真正的f2");
        Future f1 = new Future(() => null);
        f1.then((value) => print("我是f1"));
      })
      .then((value) => print(value))
      .then((value) => print("我还是f2吗"));
}

输出结果为:

我是真正的f2
我还是f2吗
我是f1

其中,每个then都会返回一个新的Future,而该future会在onValue,也就是回调函数执行时处于完成状态,然后立即执行该新future的回调函数。

稍微修改代码:
main() {
  Future f2 = new Future(() => null);

  f2.then((_) {
        print("我是真正的f2");
        Future f1 = new Future(() => null);
        f1.then((value) => print("我是f1"));
        return new Future(() => {print("全新的Future")});
      })
      .then((value) => print("我还是f2吗"))
      .then((value) => print("我不是了"));
}

运行结果为:

我是真正的f2
我是f1
全新的Future
我还是f2吗
我不是了

注意,then方法本身会返回一个future。在then中的函数也返回了一个Future,而then所返回的future会紧跟着函数返回的future之后处于完成状态再执行后续回调函数。

总结一下:

  • 当Future任务完成后,then()注册的回调函数会立即执行。需注意的是,then()注册的函数并不会添加到事件队列中,回调函数只是在事件循环中任务完成后被调用。
  • 如果Future在then()被调用之前已经完成计算,那么任务会被添加到微任务队列中,并且该任务会执行then()中注册的回调函数。
  • then会返回新的新的Future,并且该Future在onValue(then中注册的回调函数)或者onError被执行时就已经处于完成状态了。
  • 如果onValue(回调函数)返回值为一个Future,那么then返回的Future将会在onValue返回的future执行完成后处于完成状态

如何处理异步操作的结果

包括上面提到then,有三种方法处理Future的结果:

  • then: 处理操作执行结果或者错误并返回一个新的Future
  • catchError: 注册一个处理错误的回调
  • whenComplete:类似final,无论错误还是正确,Future执行结束后总是被调用

then中的onError只能处理当前Future中的错误,而catchError能处理整条调用链上的任何错误。

main() async {
  Future f1 = new Future(() => null);
  Future f2 = new Future(() => null);

  f1
      .then((value) {
        return Future.error("错误了");
      })
      .then((value) => print("执行成功了吗"), onError: (error) => print(error))
      .then((value) => Future.error('错了!'))
      .catchError((error) => {print("我也发现:$error")});
  f2.then((_) {
    print("我是f2");
  }).whenComplete(() => print("完成了"));
}

输出结果为:

错误了
我也发现:错了!
我是f2
完成了

async和await

上面讲了Future的基本用法,以及使用Future API处理数据的方法。但是这种方法存在一个问题:使用链式调用的方式把多个future连接在一起,会严重降低代码的可读性。
可以使用async和await关键字实现异步的功能。async和await可以帮助我们像写同步代码一样编写异步代码

main() async {
  Future f1 = new Future.delayed(Duration(milliseconds: 2000),() {
    return "我是第一个";
  });
  Future f2 = new Future(() {
    return "我是第二个";
  });
  f2.then((value) => print("哦哦哦"));
  print("开始了:${DateTime.now()}");
  print("${await f1}:${await f2}");
  print("结束了:${DateTime.now()}");
}

输出:

开始了:2020-10-13 15:37:16.871165
哦哦哦
我是第一个:我是第二个
结束了:2020-10-13 15:37:18.877511

注意:await只能在async函数里出现
要想改写异步代码,只需要在函数中添加async关键字

String getAString() {
  return "我是一个字符串";
}
## 改写为异步代码
Future<String> getAString() async{
  return "我是一个字符串";
}

需要注意的是,在普通函数中,return返回的为T,那么在async函数中返回的是Future<T>。但是并不需要显示的去指明返回的类型,Dart会自动将返回值包装成Future对象。但是,如果原函数返回的为Future<T>,在async函数中返回的仍然是是Future<T>。若async函数没有返回值,那么Dart会返回一个null值的Future。

main() {
  print("main函数开始了");
  firstString();
  secondString();
  thirdString();
  print("main函数结束了");
}

firstString() async{
  print("firstString函数开始了");
  Future.delayed(Duration(milliseconds: 300), () {
    return "我是一个字符串";
  }).then((value) => {print(value)});
  print("firstString函数结束了");
}

secondString() {
  print("我是二个字符串");
}

thirdString() {
  print("我是三个字符串");
}

上面代码的输出结果为:

main函数开始了
firstString函数开始了
firstString函数结束了
我是二个字符串
我是三个字符串
main函数结束了
我是一个字符串

注意观察代码的执行顺序,函数按照顺序执行,首先执行main函数,接着按照顺序执行firstString()、secondString()thirdString()。Future.delayed并不会阻碍任何代码的执行,这符合上文中讲的非阻塞调用,Future并不会阻塞它所在函数的执行。
我们稍微修改一下代码:

main() {
  print("main函数开始了");
  firstString();
  secondString();
  thirdString();
  print("main函数结束了");
}

firstString() async {
  print("firstString函数开始了");
  Future future = Future.delayed(Duration(milliseconds: 300), () {
    return "我是一个字符串";
  });
  print(await future);
  print("firstString函数结束了");
}

secondString() {
  print("我是二个字符串");
}

thirdString() {
  print("我是三个字符串");
}

输出结果为:

main函数开始了
firstString函数开始了
我是二个字符串
我是三个字符串
main函数结束了
我是一个字符串
firstString函数结束了

对比两次结果不难发现,async和await关键字使得原本非阻塞式的函数变的同步了,成了阻塞函数了。函数遇到Future,再其未执行完之前一直处于阻塞状态。但是main函数依旧正常执行,并不会被async函数所阻塞。async和await只会作用于当前函数,并不会对其他外部函数造成执行上的影响。
await也可以帮助我们在执行下个语句之前确保当前语句执行完毕:

main() async {
  print("main函数开始了:${DateTime.now()}");
  print(await firstString());
  print(await secondString());
  print(await thirdString());
  print("main函数结束了:${DateTime.now()}");
}

firstString() {
  return Future.delayed(Duration(milliseconds: 300), () {
    return "我是一个字符串";
  });
}

secondString() {
  return Future.delayed(Duration(milliseconds: 200), () {
    return "我是二个字符串";
  });
}

thirdString() {
  return Future.delayed(Duration(milliseconds: 100), () {
    return "我是三个字符串";
  });
}

输出结果为:

main函数开始了:2020-10-13 16:24:46.897353
我是一个字符串
我是二个字符串
我是三个字符串
main函数结束了:2020-10-13 16:24:47.527151
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349