前言
我們所熟悉的前端開發(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的事件循環(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
的燒腦說明。大家先感受一下:
- 你通過then串起來的那些回調(diào)函數(shù)在
Future
完成的時(shí)候會(huì)被立即執(zhí) 行祟偷,也就是說它們是同步執(zhí)行羽嫡,而不是被調(diào)度異步執(zhí)行。- 如果
Future
在調(diào)用then
串起回調(diào)函數(shù)之前已經(jīng)完成肩袍,
那么這些回調(diào)函數(shù)會(huì)被調(diào)度到微任務(wù)隊(duì)列異步執(zhí)行杭棵。- 通過
Future()
和Future.delayed()
實(shí)例化的Future
不會(huì)同步執(zhí)行,它們會(huì)被調(diào)度到事件隊(duì)列異步執(zhí)行氛赐。- 通過
Future.value()
實(shí)例化的Future
會(huì)被調(diào)度到微任務(wù)隊(duì)列異步完成魂爪,類似于第2條。- 通過
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
, catchError
和whenComplete
串上你需要的回調(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
async
和await
是什么?它們是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所示:
綠框里面的代碼會(huì)在
foo
函數(shù)被調(diào)用的時(shí)候同步執(zhí)行抽高,在遇到await
的時(shí)候器一,會(huì)馬上返回一個(gè)Future
,剩下的紅框里面的代碼以then
的方式鏈入這個(gè)Future
被異步調(diào)度執(zhí)行厨内。
上述代碼運(yùn)行以后在終端會(huì)輸出如下:
可見
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
。
使用async
和await
還有一個(gè)好處是我們可以用和同步代碼相同的try
/catch
機(jī)制來做異常處理统翩。
foo() async {
try {
print('foo E');
var value = await bar();
print('foo X $value');
} catch (e) {
// 同步執(zhí)行代碼中的異常和異步執(zhí)行代碼的異常都會(huì)被捕獲
} finally {
}
}
在日常使用場景中仙蚜,我們通常利用async
,await
來異步處理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
栗涂;再到 async
和await
知牌。了解了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ì)另寫文章來介紹一下纹磺。