Flutter/Dart中的異步

前言

我們所熟悉的前端開發(fā)框架大都是事件驅(qū)動(dòng)的。事件驅(qū)動(dòng)意味著你的程序中必然存在事件循環(huán)和事件隊(duì)列耕餐。事件循環(huán)會(huì)不停的從事件隊(duì)列中獲取和處理各種事件题禀。也就是說你的程序必然是支持異步的侮攀。

在Android中這樣的結(jié)構(gòu)是Looper/Handler丈甸;在iOS中是RunLoop;在JavaScript中是Event Loop。

同樣的Flutter/Dart也是事件驅(qū)動(dòng)的,也有自己的Event Loop。而且這個(gè)Event Loop和JavaScript的很像柑爸,很像。(畢竟Dart是想替換JS來著)盒音。下面我們就來了解一下Dart中的Event Loop表鳍。

Dart的Event Loop

Dart的事件循環(huán)如下圖所示。和JavaScript的基本一樣祥诽。循環(huán)中有兩個(gè)隊(duì)列譬圣。一個(gè)是微任務(wù)隊(duì)列(MicroTask queue),一個(gè)是事件隊(duì)列(Event queue)雄坪。

  • 事件隊(duì)列包含外部事件厘熟,例如I/O, Timer,繪制事件等等维哈。
  • 微任務(wù)隊(duì)列則包含有Dart內(nèi)部的微任務(wù)绳姨,主要是通過scheduleMicrotask來調(diào)度。
Dart的Event Loop

Dart的事件循環(huán)的運(yùn)行遵循以下規(guī)則:

  • 首先處理所有微任務(wù)隊(duì)列里的微任務(wù)阔挠。
  • 處理完所有微任務(wù)以后飘庄。從事件隊(duì)列里取1個(gè)事件進(jìn)行處理。
  • 回到微任務(wù)隊(duì)列繼續(xù)循環(huán)购撼。

注意第一步里的所有跪削,也就是說在處理事件隊(duì)列之前,Dart要先把所有的微任務(wù)處理完份招。如果某一時(shí)刻微任務(wù)隊(duì)列里有8個(gè)微任務(wù)切揭,事件隊(duì)列有2個(gè)事件狞甚,Dart也會(huì)先把這8個(gè)微任務(wù)全部處理完再從事件隊(duì)列中取出1個(gè)事件處理锁摔,之后又會(huì)回到微任務(wù)隊(duì)列去看有沒有未執(zhí)行的微任務(wù)。

總而言之哼审,就是對(duì)微任務(wù)隊(duì)列是一次性全部處理谐腰,對(duì)于事件隊(duì)列是一次只處理一個(gè)。

這個(gè)流程要清楚涩盾,清楚了才能理解Dart代碼的執(zhí)行順序十气。

異步執(zhí)行

那么在Dart中如何讓你的代碼異步執(zhí)行呢?很簡單春霍,把要異步執(zhí)行的代碼放在微任務(wù)隊(duì)列或者事件隊(duì)列里就行了砸西。

  • 可以調(diào)用scheduleMicrotask來讓代碼以微任務(wù)的方式異步執(zhí)行
    scheduleMicrotask((){
        print('a microtask');
    });
  • 可以調(diào)用Timer.run來讓代碼以Event的方式異步執(zhí)行
   Timer.run((){
       print('a event');
   });

好了,現(xiàn)在你知道怎么讓你的Dart代碼異步執(zhí)行了∏奂希看起來并不是很復(fù)雜衅疙,但是你需要清楚的知道你的異步代碼執(zhí)行的順序。這也是很多前端面試時(shí)候會(huì)問到的問題鸳慈。舉個(gè)簡單的例子饱溢,請(qǐng)問下面這段代碼是否會(huì)輸出"executed"?

main() {
     Timer.run(() { print("executed"); });  
      foo() {
        scheduleMicrotask(foo);  
      }
      foo();
    }

答案是不會(huì),因?yàn)樵谑冀K會(huì)有一個(gè)foo存在于微任務(wù)隊(duì)列走芋。導(dǎo)致Event Loop沒有機(jī)會(huì)去處理事件隊(duì)列绩郎。還有更復(fù)雜的一些例子會(huì)有大量的異步代碼混合嵌套起來然后問你執(zhí)行順序是什么樣的,這都需要按照上述Event Loop規(guī)則仔細(xì)去分析翁逞。

和JS一樣肋杖,僅僅使用回調(diào)函數(shù)來做異步的話很容易陷入“回調(diào)地獄(Callback hell)”,為了避免這樣的問題熄攘,JS引入了Promise兽愤。同樣的, Dart引入了Future

Future

要使用Future的話需要引入dart.async

import 'dart:async';

Future提供了一系列構(gòu)造函數(shù)供你選擇挪圾。

創(chuàng)建一個(gè)立刻在事件隊(duì)列里運(yùn)行的Future:

Future(() => print('立刻在Event queue中運(yùn)行的Future'));

創(chuàng)建一個(gè)延時(shí)1秒在事件隊(duì)列里運(yùn)行的Future:

Future.delayed(const Duration(seconds:1), () => print('1秒后在Event queue中運(yùn)行的Future'));

創(chuàng)建一個(gè)在微任務(wù)隊(duì)列里運(yùn)行的Future:

Future.microtask(() => print('在Microtask queue里運(yùn)行的Future'));

創(chuàng)建一個(gè)同步運(yùn)行的Future:

Future.sync(() => print('同步運(yùn)行的Future'));

對(duì)浅萧,你沒看錯(cuò),同步運(yùn)行的哲思。

這里要注意一下洼畅,這個(gè)同步運(yùn)行指的是構(gòu)造Future的時(shí)候傳入的函數(shù)是同步運(yùn)行的,這個(gè)Future通過then串進(jìn)來的回調(diào)函數(shù)是調(diào)度到微任務(wù)隊(duì)列異步執(zhí)行的棚赔。

有了Future之后, 通過調(diào)用then來把回調(diào)函數(shù)串起來帝簇,這樣就解決了"回調(diào)地獄"的問題。

Future(()=> print('task'))
    .then((_)=> print('callback1'))
    .then((_)=> print('callback2'));

在task打印完畢以后靠益,通過then串起來的回調(diào)函數(shù)會(huì)按照鏈接的順序依次執(zhí)行丧肴。
如果task執(zhí)行出錯(cuò)怎么辦?你可以通過catchError來鏈上一個(gè)錯(cuò)誤處理函數(shù):

 Future(()=> throw 'we have a problem')
      .then((_)=> print('callback1'))
      .then((_)=> print('callback2'))
      .catchError((error)=>print('$error'));

上面這個(gè)Future執(zhí)行時(shí)直接拋出一個(gè)異常胧后,這個(gè)異常會(huì)被catchError捕捉到芋浮。類似于Java中的try/catch機(jī)制的catch代碼塊。運(yùn)行后只會(huì)執(zhí)行catchError里的代碼壳快。兩個(gè)then中的代碼都不會(huì)被執(zhí)行纸巷。

既然有了類似Java的try/catch,那么Java中的finally也應(yīng)該有吧眶痰。有的瘤旨,那就是whenComplete:

Future(()=> throw 'we have a problem')
    .then((_)=> print('callback1'))
    .then((_)=> print('callback2'))
    .catchError((error)=>print('$error'))
    .whenComplete(()=> print('whenComplete'));

無論這個(gè)Future是正常執(zhí)行完畢還是拋出異常,whenComplete都一定會(huì)被執(zhí)行竖伯。

以上就是對(duì)Future的一些主要用法的介紹存哲。Future背后的實(shí)現(xiàn)機(jī)制還是有一些復(fù)雜的因宇。這里先列幾個(gè)來自Dart官網(wǎng)的關(guān)于Future的燒腦說明。大家先感受一下:

  1. 你通過then串起來的那些回調(diào)函數(shù)在Future完成的時(shí)候會(huì)被立即執(zhí) 行祟偷,也就是說它們是同步執(zhí)行羽嫡,而不是被調(diào)度異步執(zhí)行。
  2. 如果Future在調(diào)用then串起回調(diào)函數(shù)之前已經(jīng)完成肩袍,
    那么這些回調(diào)函數(shù)會(huì)被調(diào)度到微任務(wù)隊(duì)列異步執(zhí)行杭棵。
  3. 通過Future()Future.delayed()實(shí)例化的Future不會(huì)同步執(zhí)行,它們會(huì)被調(diào)度到事件隊(duì)列異步執(zhí)行氛赐。
  4. 通過Future.value()實(shí)例化的Future會(huì)被調(diào)度到微任務(wù)隊(duì)列異步完成魂爪,類似于第2條。
  5. 通過Future.sync()實(shí)例化的Future會(huì)同步執(zhí)行其入?yún)⒑瘮?shù)艰管,然后(除非這個(gè)入?yún)⒑瘮?shù)返回一個(gè)Future)調(diào)度到微任務(wù)隊(duì)列來完成自己滓侍,類似于第2條。

從上述說明可以得出結(jié)論牲芋,Future中的代碼至少會(huì)有一部分被異步調(diào)度執(zhí)行的撩笆,要么是其入?yún)⒑瘮?shù)和回調(diào)被異步調(diào)度執(zhí)行,要么就只有回調(diào)被異步調(diào)度執(zhí)行缸浦。

不知道大家注意到?jīng)]有夕冲,通過以上那些Future構(gòu)造函數(shù)生成的Future對(duì)象其實(shí)控制權(quán)不在你這里。它什么時(shí)候執(zhí)行完畢只能等系統(tǒng)調(diào)度了裂逐。你只能被動(dòng)的等待Future執(zhí)行完畢然后調(diào)用你設(shè)置的回調(diào)歹鱼。如果你想手動(dòng)控制某個(gè)Future怎么辦呢?請(qǐng)使用Completer卜高。

Completer

這里就舉個(gè)Completer的例子吧

// 實(shí)例化一個(gè)Completer
var completer = Completer();
// 這里可以拿到這個(gè)completer內(nèi)部的Future
var future = completer.future;
// 需要的話串上回調(diào)函數(shù)弥姻。
future.then((value)=> print('$value'));

//做些其它事情 
...
// 設(shè)置為完成狀態(tài)
completer.complete("done");

上述代碼片段中,當(dāng)你創(chuàng)建了一個(gè)Completer以后掺涛,其內(nèi)部會(huì)包含一個(gè)Future庭敦。你可以在這個(gè)Future上通過then, catchErrorwhenComplete串上你需要的回調(diào)。拿著這個(gè)Completer實(shí)例薪缆,在你的代碼里的合適位置秧廉,通過調(diào)用complete函數(shù)即可完成這個(gè)Completer對(duì)應(yīng)的Future“牵控制權(quán)完全在你自己的代碼手里定血。當(dāng)然你也可以通過調(diào)用completeError來以異常的方式結(jié)束這個(gè)Future赔癌。

總結(jié)就是:

  • 我創(chuàng)建的诞外,完成了調(diào)我的回調(diào)就行了: 用 Future
  • 我創(chuàng)建的灾票,得我來結(jié)束它: 用Completer峡谊。

Future相對(duì)于調(diào)度回調(diào)函數(shù)來說,緩減了回調(diào)地獄的問題。但是如果Future要串起來的的東西比較多的話既们,代碼還是會(huì)可讀性比較差濒析。特別是各種Future嵌套起來,是比較燒腦的啥纸。

所以能不能更給力一點(diǎn)呢号杏?可以的!JavaScript有 async/await斯棒,Dart也有盾致。

async/await

asyncawait是什么?它們是Dart語言的關(guān)鍵字荣暮,有了這兩個(gè)關(guān)鍵字庭惜,可以讓你用同步代碼的形式寫出異步代碼。啥意思呢穗酥?看下面這個(gè)例子:

foo() async {
  print('foo E');
  String value = await bar();
  print('foo X $value');
}

bar() async {
  print("bar E");
  return "hello";
}

main() {
  print('main E');
  foo();
  print("main X");
}

函數(shù)foo被關(guān)鍵字async修飾护赊,其內(nèi)部的有3行代碼,看起來和普通的函數(shù)沒什么兩樣砾跃。但是在第2行等號(hào)右側(cè)有個(gè)await關(guān)鍵字骏啰,await的出現(xiàn)讓看似會(huì)同步執(zhí)行的代碼裂變?yōu)閮刹糠帧H缦聢D所示:

async await

綠框里面的代碼會(huì)在foo函數(shù)被調(diào)用的時(shí)候同步執(zhí)行抽高,在遇到await的時(shí)候器一,會(huì)馬上返回一個(gè)Future,剩下的紅框里面的代碼以then的方式鏈入這個(gè)Future被異步調(diào)度執(zhí)行厨内。

上述代碼運(yùn)行以后在終端會(huì)輸出如下:

output

可見print('foo X $value')是在main執(zhí)行完畢以后才打印出來的祈秕。的確是異步執(zhí)行的。

而以上代碼中的foo函數(shù)可以以Future方式實(shí)現(xiàn)如下,兩者是等效的

foo() {
  print('foo E');
  return Future(bar).then((value) => print('foo X $value'));
}

await并不像字面意義上程序運(yùn)行到這里就停下來啥也不干等待Future完成雏胃。而是立刻結(jié)束當(dāng)前函數(shù)的執(zhí)行并返回一個(gè)Future请毛。函數(shù)內(nèi)剩余代碼通過調(diào)度異步執(zhí)行。

  • await只能在async函數(shù)中出現(xiàn)瞭亮。
  • async函數(shù)中可以出現(xiàn)多個(gè)await,每遇見一個(gè)就返回一個(gè)Future, 實(shí)際結(jié)果類似于用then串起來的回調(diào)方仿。
  • async函數(shù)也可以沒有await, 在函數(shù)體同步執(zhí)行完畢以后返回一個(gè)Future

使用asyncawait還有一個(gè)好處是我們可以用和同步代碼相同的try/catch機(jī)制來做異常處理统翩。

foo() async {
  try {
    print('foo E');
    var value = await bar();
    print('foo X $value');
  } catch (e) {
    // 同步執(zhí)行代碼中的異常和異步執(zhí)行代碼的異常都會(huì)被捕獲
  } finally {
    
  }
}

在日常使用場景中仙蚜,我們通常利用asyncawait來異步處理IO厂汗,網(wǎng)絡(luò)請(qǐng)求委粉,以及Flutter中的Platform channels通信等耗時(shí)操作。

總結(jié)

本文大致介紹了Flutter/Dart中的異步運(yùn)行機(jī)制娶桦,從異步運(yùn)行的基礎(chǔ)(Event Loop)開始贾节,首先介紹了最原始的異步運(yùn)行機(jī)制汁汗,直接調(diào)度回調(diào)函數(shù);到Future栗涂;再到 asyncawait知牌。了解了Flutter/Dart中的異步運(yùn)行機(jī)制是如何一步一步的進(jìn)化而來的。對(duì)于一直從事Native開發(fā)斤程,不太了解JavaScrip的同學(xué)來講角寸,這個(gè)異步機(jī)制和原生開發(fā)有很大的不同,需要多多動(dòng)手練習(xí)忿墅,動(dòng)腦思考才能適應(yīng)袭厂。本文中介紹的相關(guān)知識(shí)點(diǎn)較為粗淺,并沒有涉及dart:async中關(guān)于Future實(shí)現(xiàn)的源碼分析以及Stream等不太常用的類球匕。這些如果大家想了解一下的話我會(huì)另寫文章來介紹一下纹磺。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市亮曹,隨后出現(xiàn)的幾起案子橄杨,更是在濱河造成了極大的恐慌,老刑警劉巖照卦,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件式矫,死亡現(xiàn)場離奇詭異,居然都是意外死亡役耕,警方通過查閱死者的電腦和手機(jī)采转,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來瞬痘,“玉大人故慈,你說我怎么就攤上這事】蛉” “怎么了察绷?”我有些...
    開封第一講書人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長津辩。 經(jīng)常有香客問我拆撼,道長,這世上最難降的妖魔是什么喘沿? 我笑而不...
    開封第一講書人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任闸度,我火速辦了婚禮,結(jié)果婚禮上蚜印,老公的妹妹穿的比我還像新娘莺禁。我一直安慰自己,他們只是感情好晒哄,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開白布睁宰。 她就那樣靜靜地躺著,像睡著了一般寝凌。 火紅的嫁衣襯著肌膚如雪柒傻。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,185評(píng)論 1 284
  • 那天较木,我揣著相機(jī)與錄音红符,去河邊找鬼。 笑死伐债,一個(gè)胖子當(dāng)著我的面吹牛预侯,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播峰锁,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼萎馅,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了虹蒋?” 一聲冷哼從身側(cè)響起糜芳,我...
    開封第一講書人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎魄衅,沒想到半個(gè)月后峭竣,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡晃虫,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年皆撩,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片哲银。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡扛吞,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出荆责,到底是詐尸還是另有隱情喻粹,我是刑警寧澤,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布草巡,位于F島的核電站守呜,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏山憨。R本人自食惡果不足惜查乒,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望郁竟。 院中可真熱鬧玛迄,春花似錦、人聲如沸棚亩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至勒虾,卻和暖如春纺阔,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背修然。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來泰國打工笛钝, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人愕宋。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓玻靡,卻偏偏與公主長得像,于是被迫代替她去往敵國和親中贝。 傳聞我的和親對(duì)象是個(gè)殘疾皇子囤捻,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容