Flutter中的異步編程——Future

Dart是一個在單線程中運行的程序骨杂,這意味著:如果程序在執(zhí)行中遇到一個需要長時間的執(zhí)行的操作涂身,程序?qū)粌鼋Y(jié)雄卷。為了避免造成程序的凍結(jié)搓蚪,可以使用異步操作使程序在等待一個耗時操作完成時繼續(xù)處理其他工作。在Dart中丁鹉,可以使用Future對象來表示異步操作的結(jié)果妒潭。

Dart的消息循環(huán)機制

在進入正題之前,我們先看一下Dart的消息循環(huán)機制:


簡單總結(jié)一下揣钦,詳細內(nèi)容可以看文章The Event Loop and Dart

Dart中事件循環(huán)的一些主要概念:

  • Dart從兩個隊列執(zhí)行任務(wù):event事件隊列和microtask微任務(wù)隊列雳灾;
  • Dart的方法是不會被其他Dart代碼打斷的,當main執(zhí)行完成后冯凹,main isolate的線程就會去逐一處理消息隊列中的消息
  • 事件隊列具有來自Dart(Future谎亩,Timer,Isolate Message等)和系統(tǒng)(用戶輸入,I/O等)匈庭;
  • 微任務(wù)隊列目前僅包含來自Dart;
  • 事件循環(huán)會優(yōu)先處理微任務(wù)隊列夫凸,microtask清空之后才將event事件隊列中的下一個項目出隊并處理。
  • 一旦兩個隊列都為空阱持,則應(yīng)用程序已完成工作夭拌,并且(取決于其嵌入程序)可以退出。
  • main()函數(shù)以及微任務(wù)和事件隊列中的所有項目都在Dart應(yīng)用程序的main isolate上運行衷咽。

什么是Future

Future<T>表示一個指定類型的異步操作結(jié)果(不需要結(jié)果可以使用Future<void>)當一個返回 future 對象的函數(shù)被調(diào)用時:

  1. 講函數(shù)放入隊列等待執(zhí)行并返回一個未完成的Future對象

  2. 當函數(shù)操作執(zhí)行完成鸽扁,F(xiàn)uture對象變?yōu)橥瓿刹y帶一個值或一個錯誤
    上面兩條分別對應(yīng)兩個狀態(tài):

  3. 運行狀態(tài)(pending),表示任務(wù)還未完成镶骗,也沒有返回值

  4. 完成狀態(tài)(completed),表示任務(wù)已經(jīng)結(jié)束(無論失敗還是成功)

例如:

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

觀察程序輸出桶现,首先執(zhí)行完main函數(shù)然后再去執(zhí)行任務(wù)棧中的內(nèi)容,在該例中也就是我們使用Future假如到event任務(wù)棧中的任務(wù)<u>鼎姊,then中的方法會在Future處于完成態(tài)(completed)時立馬執(zhí)行</u>巩那,之后我們再詳細講解。
Dart提供了數(shù)種創(chuàng)建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中所使用的就是這種方式創(chuàng)建的Future即横。
其他創(chuàng)建Future的方式包括:

  • Future.value():返回一個指定值的Future
  • Future.delayed():返回一個延時執(zhí)行的Future
main() {
  Future.delayed(Duration(milliseconds: 200),(){
    print("我是延遲的Future");
  });
  var future = Future.value("我是Future");
  future.then((value) => print(value));
}
# print:
# 我是Future
# 我是延遲的Future

這端代碼執(zhí)行了兩個分支:

  • main()方法
  • event隊列

Future中的任務(wù)調(diào)度

前面講過:當Future執(zhí)行完成后,then()注冊的回調(diào)函數(shù)會立即執(zhí)行裆赵,但是then中的函數(shù)并不會被添加到事件隊列中东囚,只是在事件隊列中的任務(wù)被執(zhí)行完成后才被立刻執(zhí)行(可以理解為:將網(wǎng)絡(luò)請求放在隊列中進行執(zhí)行,拿到結(jié)果后在then中刷新UI)战授。

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

上面程序的輸出結(jié)果為:

我是f1
我是f2
創(chuàng)建f3
我是f3

首先页藻,任務(wù)棧符合以FIFO的方式運行,f1,f2,f3一次被加入到任務(wù)棧植兰,then()注冊的函數(shù)并不會被添加到隊列份帐,也不會直接運行。當任務(wù)棧中任務(wù)被執(zhí)行后楣导,立刻運行then中的函數(shù)废境,依次類推⊥卜保可以看到噩凹,then中的回調(diào)函數(shù)執(zhí)行的順序并不取決于注冊的順序,而僅僅與其Future被加入到任務(wù)棧的順序有關(guān)毡咏。
注意:new Future(() => null)和new Future(null)有本質(zhì)上的區(qū)別驮宴,一個函數(shù)體為空,什么都不做呕缭;一個是參數(shù)為空堵泽,不存在函數(shù)修己。
稍微修改一下上例中的代碼:

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

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

執(zhí)行結(jié)果為:

我是f2
我還是f2
我是f1
創(chuàng)建f3
我是f3
我是一個新的

先看一下then的定義:

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

這里涉及到兩個關(guān)鍵點:

  • 如果Future在then被調(diào)用之前已經(jīng)完成,那么then中的函數(shù)會被作為任務(wù)添加到microtask隊列中迎罗;
  • then會返回新的新的Future箩退,并且該Future在onValue(then中注冊的回調(diào)函數(shù))或者onError被執(zhí)行時就已經(jīng)處于完成狀態(tài)了。
  • 如果onValue(回調(diào)函數(shù))返回值為一個Future佳谦,那么then返回的Future將會在onValue返回的future執(zhí)行完成后處于完成狀態(tài)
    關(guān)于后面兩點:
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嗎"));
}

輸出結(jié)果為:

我是真正的f2
我還是f2嗎
我是f1

其中戴涝,每個then都會返回一個新的Future,而該future會在onValue钻蔑,也就是回調(diào)函數(shù)執(zhí)行時處于完成狀態(tài)啥刻,然后立即執(zhí)行該新future的回調(diào)函數(shù)。

稍微修改代碼:
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("我不是了"));
}

運行結(jié)果為:

我是真正的f2
我是f1
全新的Future
我還是f2嗎
我不是了

注意咪笑,then方法本身會返回一個future可帽。在then中的函數(shù)也返回了一個Future,而then所返回的future會緊跟著函數(shù)返回的future之后處于完成狀態(tài)再執(zhí)行后續(xù)回調(diào)函數(shù)窗怒。

總結(jié)一下:

  • 當Future任務(wù)完成后映跟,then()注冊的回調(diào)函數(shù)會立即執(zhí)行。需注意的是扬虚,then()注冊的函數(shù)并不會添加到事件隊列中努隙,回調(diào)函數(shù)只是在事件循環(huán)中任務(wù)完成后被調(diào)用。
  • 如果Future在then()被調(diào)用之前已經(jīng)完成計算辜昵,那么任務(wù)會被添加到微任務(wù)隊列中荸镊,并且該任務(wù)會執(zhí)行then()中注冊的回調(diào)函數(shù)。
  • then會返回新的新的Future堪置,并且該Future在onValue(then中注冊的回調(diào)函數(shù))或者onError被執(zhí)行時就已經(jīng)處于完成狀態(tài)了躬存。
  • 如果onValue(回調(diào)函數(shù))返回值為一個Future,那么then返回的Future將會在onValue返回的future執(zhí)行完成后處于完成狀態(tài)

如何處理異步操作的結(jié)果

包括上面提到then舀锨,有三種方法處理Future的結(jié)果:

  • then: 處理操作執(zhí)行結(jié)果或者錯誤并返回一個新的Future
  • catchError: 注冊一個處理錯誤的回調(diào)
  • whenComplete:類似final岭洲,無論錯誤還是正確,F(xiàn)uture執(zhí)行結(jié)束后總是被調(diào)用

then中的onError只能處理當前Future中的錯誤坎匿,而catchError能處理整條調(diào)用鏈上的任何錯誤盾剩。

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

  f1
      .then((value) {
        return Future.error("錯誤了");
      })
      .then((value) => print("執(zhí)行成功了嗎"), onError: (error) => print(error))
      .then((value) => Future.error('錯了!'))
      .catchError((error) => {print("我也發(fā)現(xiàn):$error")});
  f2.then((_) {
    print("我是f2");
  }).whenComplete(() => print("完成了"));
}

輸出結(jié)果為:

錯誤了
我也發(fā)現(xiàn):錯了!
我是f2
完成了

async和await

上面講了Future的基本用法碑诉,以及使用Future API處理數(shù)據(jù)的方法彪腔。但是這種方法存在一個問題:使用鏈式調(diào)用的方式把多個future連接在一起侥锦,會嚴重降低代碼的可讀性进栽。
可以使用async和await關(guān)鍵字實現(xiàn)異步的功能。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("結(jié)束了:${DateTime.now()}");
}

輸出:

開始了:2020-10-13 15:37:16.871165
哦哦哦
我是第一個:我是第二個
結(jié)束了:2020-10-13 15:37:18.877511

注意:await只能在async函數(shù)里出現(xiàn)
要想改寫異步代碼恭垦,只需要在函數(shù)中添加async關(guān)鍵字

String getAString() {
  return "我是一個字符串";
}
## 改寫為異步代碼
Future<String> getAString() async{
  return "我是一個字符串";
}

需要注意的是快毛,在普通函數(shù)中格嗅,return返回的為T,那么在async函數(shù)中返回的是Future<T>唠帝。但是并不需要顯示的去指明返回的類型屯掖,Dart會自動將返回值包裝成Future對象。但是襟衰,如果原函數(shù)返回的為Future<T>贴铜,在async函數(shù)中返回的仍然是是Future<T>。若async函數(shù)沒有返回值瀑晒,那么Dart會返回一個null值的Future绍坝。

main() {
  print("main函數(shù)開始了");
  firstString();
  secondString();
  thirdString();
  print("main函數(shù)結(jié)束了");
}

firstString() async{
  print("firstString函數(shù)開始了");
  Future.delayed(Duration(milliseconds: 300), () {
    return "我是一個字符串";
  }).then((value) => {print(value)});
  print("firstString函數(shù)結(jié)束了");
}

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

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

上面代碼的輸出結(jié)果為:

main函數(shù)開始了
firstString函數(shù)開始了
firstString函數(shù)結(jié)束了
我是二個字符串
我是三個字符串
main函數(shù)結(jié)束了
我是一個字符串

注意觀察代碼的執(zhí)行順序,函數(shù)按照順序執(zhí)行苔悦,首先執(zhí)行main函數(shù)轩褐,接著按照順序執(zhí)行firstString()、secondString()thirdString()玖详。Future.delayed并不會阻礙任何代碼的執(zhí)行把介,這符合上文中講的非阻塞調(diào)用,F(xiàn)uture并不會阻塞它所在函數(shù)的執(zhí)行蟋座。
我們稍微修改一下代碼:

main() {
  print("main函數(shù)開始了");
  firstString();
  secondString();
  thirdString();
  print("main函數(shù)結(jié)束了");
}

firstString() async {
  print("firstString函數(shù)開始了");
  Future future = Future.delayed(Duration(milliseconds: 300), () {
    return "我是一個字符串";
  });
  print(await future);
  print("firstString函數(shù)結(jié)束了");
}

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

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

輸出結(jié)果為:

main函數(shù)開始了
firstString函數(shù)開始了
我是二個字符串
我是三個字符串
main函數(shù)結(jié)束了
我是一個字符串
firstString函數(shù)結(jié)束了

對比兩次結(jié)果不難發(fā)現(xiàn)拗踢,async和await關(guān)鍵字使得原本非阻塞式的函數(shù)變的同步了,成了阻塞函數(shù)了向臀。函數(shù)遇到Future秒拔,再其未執(zhí)行完之前一直處于阻塞狀態(tài)。但是main函數(shù)依舊正常執(zhí)行飒硅,并不會被async函數(shù)所阻塞砂缩。async和await只會作用于當前函數(shù),并不會對其他外部函數(shù)造成執(zhí)行上的影響三娩。
await也可以幫助我們在執(zhí)行下個語句之前確保當前語句執(zhí)行完畢:

main() async {
  print("main函數(shù)開始了:${DateTime.now()}");
  print(await firstString());
  print(await secondString());
  print(await thirdString());
  print("main函數(shù)結(jié)束了:${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 "我是三個字符串";
  });
}

輸出結(jié)果為:

main函數(shù)開始了:2020-10-13 16:24:46.897353
我是一個字符串
我是二個字符串
我是三個字符串
main函數(shù)結(jié)束了:2020-10-13 16:24:47.527151
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末庵芭,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子雀监,更是在濱河造成了極大的恐慌双吆,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件会前,死亡現(xiàn)場離奇詭異好乐,居然都是意外死亡,警方通過查閱死者的電腦和手機瓦宜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進店門蔚万,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人临庇,你說我怎么就攤上這事反璃£腔牛” “怎么了?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵淮蜈,是天一觀的道長斋攀。 經(jīng)常有香客問我,道長梧田,這世上最難降的妖魔是什么淳蔼? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮裁眯,結(jié)果婚禮上肖方,老公的妹妹穿的比我還像新娘。我一直安慰自己未状,他們只是感情好俯画,可當我...
    茶點故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著司草,像睡著了一般艰垂。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上埋虹,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天猜憎,我揣著相機與錄音,去河邊找鬼搔课。 笑死胰柑,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的爬泥。 我是一名探鬼主播柬讨,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼袍啡!你這毒婦竟也來了踩官?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤境输,失蹤者是張志新(化名)和其女友劉穎蔗牡,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體嗅剖,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡辩越,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年馆匿,在試婚紗的時候發(fā)現(xiàn)自己被綠了撒遣。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片送淆。...
    茶點故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡猿推,死狀恐怖破托,靈堂內(nèi)的尸體忽然破棺而出哼勇,到底是詐尸還是另有隱情契沫,我是刑警寧澤捧毛,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布欺旧,位于F島的核電站姑丑,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏辞友。R本人自食惡果不足惜栅哀,卻給世界環(huán)境...
    茶點故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望称龙。 院中可真熱鬧留拾,春花似錦、人聲如沸鲫尊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽疫向。三九已至咳蔚,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間搔驼,已是汗流浹背谈火。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留舌涨,地道東北人糯耍。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像囊嘉,于是被迫代替她去往敵國和親温技。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,573評論 2 353