前言
我們所熟悉的前端開(kāi)發(fā)框架大都是事件驅(qū)動(dòng)的。事件驅(qū)動(dòng)意味著你的程序中必然存在事件循環(huán)和事件隊(duì)列。事件循環(huán)會(huì)不停的從事件隊(duì)列中獲取和處理各種事件。也就是說(shuō)你的程序必然是支持異步的。
在Android中這樣的結(jié)構(gòu)是Looper/Handler;在iOS中是RunLoop;在JavaScript中是Event Loop。
同樣的Flutter/Dart也是事件驅(qū)動(dòng)的,也有自己的Event Loop。而且這個(gè)Event Loop和JavaScript的很像,很像。(畢竟Dart是想替換JS來(lái)著)。下面我們就來(lái)了解一下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ù),主要是通過(guò)
scheduleMicrotask來(lái)調(diào)度。
Dart的事件循環(huán)的運(yùn)行遵循以下規(guī)則:
- 首先處理所有微任務(wù)隊(duì)列里的微任務(wù)。
- 處理完所有微任務(wù)以后。從事件隊(duì)列里取1個(gè)事件進(jìn)行處理。
- 回到微任務(wù)隊(duì)列繼續(xù)循環(huán)。
注意第一步里的所有,也就是說(shuō)在處理事件隊(duì)列之前,Dart要先把所有的微任務(wù)處理完。如果某一時(shí)刻微任務(wù)隊(duì)列里有8個(gè)微任務(wù),事件隊(duì)列有2個(gè)事件,Dart也會(huì)先把這8個(gè)微任務(wù)全部處理完再?gòu)氖录?duì)列中取出1個(gè)事件處理,之后又會(huì)回到微任務(wù)隊(duì)列去看有沒(méi)有未執(zhí)行的微任務(wù)。
總而言之,就是對(duì)微任務(wù)隊(duì)列是一次性全部處理,對(duì)于事件隊(duì)列是一次只處理一個(gè)。
這個(gè)流程要清楚,清楚了才能理解Dart代碼的執(zhí)行順序。
異步執(zhí)行
那么在Dart中如何讓你的代碼異步執(zhí)行呢?很簡(jiǎn)單,把要異步執(zhí)行的代碼放在微任務(wù)隊(duì)列或者事件隊(duì)列里就行了。
- 可以調(diào)用
scheduleMicrotask來(lái)讓代碼以微任務(wù)的方式異步執(zhí)行
scheduleMicrotask((){
print('a microtask');
});
- 可以調(diào)用
Timer.run來(lái)讓代碼以Event的方式異步執(zhí)行
Timer.run((){
print('a event');
});
好了,現(xiàn)在你知道怎么讓你的Dart代碼異步執(zhí)行了。看起來(lái)并不是很復(fù)雜,但是你需要清楚的知道你的異步代碼執(zhí)行的順序。這也是很多前端面試時(shí)候會(huì)問(wèn)到的問(wèn)題。舉個(gè)簡(jiǎn)單的例子,請(qǐng)問(wèn)下面這段代碼是否會(huì)輸出"executed"?
main() {
Timer.run(() { print("executed"); });
foo() {
scheduleMicrotask(foo);
}
foo();
}
答案是不會(huì),因?yàn)樵谑冀K會(huì)有一個(gè)foo存在于微任務(wù)隊(duì)列。導(dǎo)致Event Loop沒(méi)有機(jī)會(huì)去處理事件隊(duì)列。還有更復(fù)雜的一些例子會(huì)有大量的異步代碼混合嵌套起來(lái)然后問(wèn)你執(zhí)行順序是什么樣的,這都需要按照上述Event Loop規(guī)則仔細(xì)去分析。
和JS一樣,僅僅使用回調(diào)函數(shù)來(lái)做異步的話很容易陷入“回調(diào)地獄(Callback hell)”,為了避免這樣的問(wèn)題,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ì),你沒(méi)看錯(cuò),同步運(yùn)行的。
這里要注意一下,這個(gè)同步運(yùn)行指的是構(gòu)造
Future的時(shí)候傳入的函數(shù)是同步運(yùn)行的,這個(gè)Future通過(guò)then串進(jìn)來(lái)的回調(diào)函數(shù)是調(diào)度到微任務(wù)隊(duì)列異步執(zhí)行的。
有了Future之后, 通過(guò)調(diào)用then來(lái)把回調(diào)函數(shù)串起來(lái),這樣就解決了"回調(diào)地獄"的問(wèn)題。
Future(()=> print('task'))
.then((_)=> print('callback1'))
.then((_)=> print('callback2'));
在task打印完畢以后,通過(guò)then串起來(lái)的回調(diào)函數(shù)會(huì)按照鏈接的順序依次執(zhí)行。
如果task執(zhí)行出錯(cuò)怎么辦?你可以通過(guò)catchError來(lái)鏈上一個(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'));
無(wú)論這個(gè)Future是正常執(zhí)行完畢還是拋出異常,whenComplete都一定會(huì)被執(zhí)行。
以上就是對(duì)Future的一些主要用法的介紹。Future背后的實(shí)現(xiàn)機(jī)制還是有一些復(fù)雜的。這里先列幾個(gè)來(lái)自Dart官網(wǎng)的關(guān)于Future的燒腦說(shuō)明。大家先感受一下:
- 你通過(guò)then串起來(lái)的那些回調(diào)函數(shù)在
Future完成的時(shí)候會(huì)被立即執(zhí) 行,也就是說(shuō)它們是同步執(zhí)行,而不是被調(diào)度異步執(zhí)行。- 如果
Future在調(diào)用then串起回調(diào)函數(shù)之前已經(jīng)完成,
那么這些回調(diào)函數(shù)會(huì)被調(diào)度到微任務(wù)隊(duì)列異步執(zhí)行。- 通過(guò)
Future()和Future.delayed()實(shí)例化的Future不會(huì)同步執(zhí)行,它們會(huì)被調(diào)度到事件隊(duì)列異步執(zhí)行。- 通過(guò)
Future.value()實(shí)例化的Future會(huì)被調(diào)度到微任務(wù)隊(duì)列異步完成,類似于第2條。- 通過(guò)
Future.sync()實(shí)例化的Future會(huì)同步執(zhí)行其入?yún)⒑瘮?shù),然后(除非這個(gè)入?yún)⒑瘮?shù)返回一個(gè)Future)調(diào)度到微任務(wù)隊(duì)列來(lái)完成自己,類似于第2條。
從上述說(shuō)明可以得出結(jié)論,Future中的代碼至少會(huì)有一部分被異步調(diào)度執(zhí)行的,要么是其入?yún)⒑瘮?shù)和回調(diào)被異步調(diào)度執(zhí)行,要么就只有回調(diào)被異步調(diào)度執(zhí)行。
不知道大家注意到?jīng)]有,通過(guò)以上那些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上通過(guò)then, catchError和whenComplete串上你需要的回調(diào)。拿著這個(gè)Completer實(shí)例,在你的代碼里的合適位置,通過(guò)調(diào)用complete函數(shù)即可完成這個(gè)Completer對(duì)應(yīng)的Future。控制權(quán)完全在你自己的代碼手里。當(dāng)然你也可以通過(guò)調(diào)用completeError來(lái)以異常的方式結(jié)束這個(gè)Future。
總結(jié)就是:
- 我創(chuàng)建的,完成了調(diào)我的回調(diào)就行了: 用
Future。 - 我創(chuàng)建的,得我來(lái)結(jié)束它: 用
Completer。
Future相對(duì)于調(diào)度回調(diào)函數(shù)來(lái)說(shuō),緩減了回調(diào)地獄的問(wèn)題。但是如果Future要串起來(lái)的的東西比較多的話,代碼還是會(huì)可讀性比較差。特別是各種Future嵌套起來(lái),是比較燒腦的。
所以能不能更給力一點(diǎn)呢?可以的!JavaScript有 async/await,Dart也有。
async/await
async和await是什么?它們是Dart語(yǔ)言的關(guān)鍵字,有了這兩個(gè)關(guān)鍵字,可以讓你用同步代碼的形式寫(xiě)出異步代碼。啥意思呢?看下面這個(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行代碼,看起來(lái)和普通的函數(shù)沒(méi)什么兩樣。但是在第2行等號(hào)右側(cè)有個(gè)await關(guān)鍵字,await的出現(xiàn)讓看似會(huì)同步執(zhí)行的代碼裂變?yōu)閮刹糠?。如下圖所示:
綠框里面的代碼會(huì)在
foo函數(shù)被調(diào)用的時(shí)候同步執(zhí)行,在遇到await的時(shí)候,會(huì)馬上返回一個(gè)Future,剩下的紅框里面的代碼以then的方式鏈入這個(gè)Future被異步調(diào)度執(zhí)行。
上述代碼運(yùn)行以后在終端會(huì)輸出如下:
可見(jiàn)
print('foo X $value')是在main執(zhí)行完畢以后才打印出來(lái)的。的確是異步執(zhí)行的。
而以上代碼中的foo函數(shù)可以以Future方式實(shí)現(xiàn)如下,兩者是等效的
foo() {
print('foo E');
return Future(bar).then((value) => print('foo X $value'));
}
await并不像字面意義上程序運(yùn)行到這里就停下來(lái)啥也不干等待Future完成。而是立刻結(jié)束當(dāng)前函數(shù)的執(zhí)行并返回一個(gè)Future。函數(shù)內(nèi)剩余代碼通過(guò)調(diào)度異步執(zhí)行。
-
await只能在async函數(shù)中出現(xiàn)。 -
async函數(shù)中可以出現(xiàn)多個(gè)await,每遇見(jiàn)一個(gè)就返回一個(gè)Future, 實(shí)際結(jié)果類似于用then串起來(lái)的回調(diào)。 -
async函數(shù)也可以沒(méi)有await, 在函數(shù)體同步執(zhí)行完畢以后返回一個(gè)Future。
使用async和await還有一個(gè)好處是我們可以用和同步代碼相同的try/catch機(jī)制來(lái)做異常處理。
foo() async {
try {
print('foo E');
var value = await bar();
print('foo X $value');
} catch (e) {
// 同步執(zhí)行代碼中的異常和異步執(zhí)行代碼的異常都會(huì)被捕獲
} finally {
}
}
在日常使用場(chǎng)景中,我們通常利用async,await來(lái)異步處理IO,網(wǎng)絡(luò)請(qǐng)求,以及Flutter中的Platform channels通信等耗時(shí)操作。
總結(jié)
本文大致介紹了Flutter/Dart中的異步運(yùn)行機(jī)制,從異步運(yùn)行的基礎(chǔ)(Event Loop)開(kāi)始,首先介紹了最原始的異步運(yùn)行機(jī)制,直接調(diào)度回調(diào)函數(shù);到Future;再到 async和await。了解了Flutter/Dart中的異步運(yùn)行機(jī)制是如何一步一步的進(jìn)化而來(lái)的。對(duì)于一直從事Native開(kāi)發(fā),不太了解JavaScrip的同學(xué)來(lái)講,這個(gè)異步機(jī)制和原生開(kāi)發(fā)有很大的不同,需要多多動(dòng)手練習(xí),動(dòng)腦思考才能適應(yīng)。本文中介紹的相關(guān)知識(shí)點(diǎn)較為粗淺,并沒(méi)有涉及dart:async中關(guān)于Future實(shí)現(xiàn)的源碼分析以及Stream等不太常用的類。這些如果大家想了解一下的話我會(huì)另寫(xiě)文章來(lái)介紹一下。