咱們所熟悉的前端開發框架大都是事件驅動的。事件驅動意味着你的程序中必然存在事件循環和事件隊列。事件循環會不停的從事件隊列中獲取和處理各類事件。也就是說你的程序必然是支持異步的。前端
在Android中這樣的結構是Looper/Handler;在iOS中是RunLoop;在JavaScript中是Event Loop。面試
一樣的Flutter/Dart也是事件驅動的,也有本身的Event Loop。並且這個Event Loop和JavaScript的很像,很像。(畢竟Dart是想替換JS來着)。下面咱們就來了解一下Dart中的Event Loop。網絡
Dart的事件循環以下圖所示。和JavaScript的基本同樣。循環中有兩個隊列。一個是微任務隊列(MicroTask queue),一個是事件隊列(Event queue)。框架
Timer
,繪製事件等等。scheduleMicrotask
來調度。Dart的事件循環的運行遵循如下規則:異步
注意第一步裏的全部,也就是說在處理事件隊列以前,Dart要先把全部的微任務處理完。若是某一時刻微任務隊列裏有8個微任務,事件隊列有2個事件,Dart也會先把這8個微任務所有處理完再從事件隊列中取出1個事件處理,以後又會回到微任務隊列去看有沒有未執行的微任務。async
總而言之,就是對微任務隊列是一次性所有處理,對於事件隊列是一次只處理一個。函數
這個流程要清楚,清楚了才能理解Dart代碼的執行順序。oop
那麼在Dart中如何讓你的代碼異步執行呢?很簡單,把要異步執行的代碼放在微任務隊列或者事件隊列裏就好了。源碼分析
scheduleMicrotask
來讓代碼以微任務的方式異步執行scheduleMicrotask((){
print('a microtask');
});
複製代碼
Timer.run
來讓代碼以Event的方式異步執行Timer.run((){
print('a event');
});
複製代碼
好了,如今你知道怎麼讓你的Dart代碼異步執行了。看起來並非很複雜,可是你須要清楚的知道你的異步代碼執行的順序。這也是不少前端面試時候會問到的問題。舉個簡單的例子,請問下面這段代碼是否會輸出"executed"?ui
main() {
Timer.run(() { print("executed"); });
foo() {
scheduleMicrotask(foo);
}
foo();
}
複製代碼
答案是不會,由於在始終會有一個foo
存在於微任務隊列。致使Event Loop沒有機會去處理事件隊列。還有更復雜的一些例子會有大量的異步代碼混合嵌套起來而後問你執行順序是什麼樣的,這都須要按照上述Event Loop規則仔細去分析。
和JS同樣,僅僅使用回調函數來作異步的話很容易陷入「回調地獄(Callback hell)」,爲了不這樣的問題,JS引入了Promise
。一樣的, Dart引入了Future
。
要使用Future
的話須要引入dart.async
import 'dart:async';
複製代碼
Future
提供了一系列構造函數供你選擇。
建立一個馬上在事件隊列裏運行的Future
:
Future(() => print('馬上在Event queue中運行的Future'));
複製代碼
建立一個延時1秒在事件隊列裏運行的Future
:
Future.delayed(const Duration(seconds:1), () => print('1秒後在Event queue中運行的Future'));
複製代碼
建立一個在微任務隊列裏運行的Future
:
Future.microtask(() => print('在Microtask queue裏運行的Future'));
複製代碼
建立一個同步運行的Future:
Future.sync(() => print('同步運行的Future'));
複製代碼
對,你沒看錯,同步運行的。
這裏要注意一下,這個同步運行指的是構造
Future
的時候傳入的函數是同步運行的,這個Future
經過then
串進來的回調函數是調度到微任務隊列異步執行的。
有了Future
以後, 經過調用then
來把回調函數串起來,這樣就解決了"回調地獄"的問題。
Future(()=> print('task'))
.then((_)=> print('callback1'))
.then((_)=> print('callback2'));
複製代碼
在task打印完畢之後,經過then
串起來的回調函數會按照連接的順序依次執行。 若是task執行出錯怎麼辦?你能夠經過catchError
來鏈上一個錯誤處理函數:
Future(()=> throw 'we have a problem')
.then((_)=> print('callback1'))
.then((_)=> print('callback2'))
.catchError((error)=>print('$error'));
複製代碼
上面這個Future
執行時直接拋出一個異常,這個異常會被catchError
捕捉到。相似於Java中的try/catch
機制的catch
代碼塊。運行後只會執行catchError
裏的代碼。兩個then
中的代碼都不會被執行。
既然有了相似Java的try/catch
,那麼Java中的finally
也應該有吧。有的,那就是whenComplete
:
Future(()=> throw 'we have a problem')
.then((_)=> print('callback1'))
.then((_)=> print('callback2'))
.catchError((error)=>print('$error'))
.whenComplete(()=> print('whenComplete'));
複製代碼
不管這個Future
是正常執行完畢仍是拋出異常,whenComplete
都必定會被執行。
以上就是對Future
的一些主要用法的介紹。Future
背後的實現機制仍是有一些複雜的。這裏先列幾個來自Dart官網的關於Future
的燒腦說明。你們先感覺一下:
- 你經過then串起來的那些回調函數在
Future
完成的時候會被當即執 行,也就是說它們是同步執行,而不是被調度異步執行。- 若是
Future
在調用then
串起回調函數以前已經完成,
那麼這些回調函數會被調度到微任務隊列異步執行。- 經過
Future()
和Future.delayed()
實例化的Future
不會同步執行,它們會被調度到事件隊列異步執行。- 經過
Future.value()
實例化的Future
會被調度到微任務隊列異步完成,相似於第2條。- 經過
Future.sync()
實例化的Future
會同步執行其入參函數,而後(除非這個入參函數返回一個Future
)調度到微任務隊列來完成本身,相似於第2條。
從上述說明能夠得出結論,Future
中的代碼至少會有一部分被異步調度執行的,要麼是其入參函數和回調被異步調度執行,要麼就只有回調被異步調度執行。
不知道你們注意到沒有,經過以上那些Future
構造函數生成的Future
對象其實控制權不在你這裏。它何時執行完畢只能等系統調度了。你只能被動的等待Future
執行完畢而後調用你設置的回調。若是你想手動控制某個Future
怎麼辦呢?請使用Completer
。
這裏就舉個Completer
的例子吧
// 實例化一個Completer
var completer = Completer();
// 這裏能夠拿到這個completer內部的Future
var future = completer.future;
// 須要的話串上回調函數。
future.then((value)=> print('$value'));
//作些其它事情
...
// 設置爲完成狀態
completer.complete("done");
複製代碼
上述代碼片斷中,當你建立了一個Completer
之後,其內部會包含一個Future
。你能夠在這個Future
上經過then
, catchError
和whenComplete
串上你須要的回調。拿着這個Completer
實例,在你的代碼裏的合適位置,經過調用complete
函數便可完成這個Completer
對應的Future
。控制權徹底在你本身的代碼手裏。固然你也能夠經過調用completeError
來以異常的方式結束這個Future
。
總結就是:
Future
。Completer
。Future
相對於調度回調函數來講,緩減了回調地獄的問題。可是若是Future
要串起來的的東西比較多的話,代碼仍是會可讀性比較差。特別是各類Future
嵌套起來,是比較燒腦的。
因此能不能更給力一點呢?能夠的!JavaScript有 async/await,Dart也有。
async
和await
是什麼?它們是Dart語言的關鍵字,有了這兩個關鍵字,可讓你用同步代碼的形式寫出異步代碼。啥意思呢?看下面這個例子:
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");
}
複製代碼
函數foo
被關鍵字async
修飾,其內部的有3行代碼,看起來和普通的函數沒什麼兩樣。可是在第2行等號右側有個await
關鍵字,await
的出現讓看似會同步執行的代碼裂變爲兩部分。以下圖所示:
foo
函數被調用的時候同步執行,在遇到
await
的時候,會立刻返回一個
Future
,剩下的紅框裏面的代碼以
then
的方式鏈入這個
Future
被異步調度執行。
上述代碼運行之後在終端會輸出以下:
可見print('foo X $value')
是在
main
執行完畢之後纔打印出來的。的確是異步執行的。
而以上代碼中的foo
函數能夠以Future
方式實現以下,二者是等效的
foo() {
print('foo E');
return Future.sync(bar).then((value) => print('foo X $value'));
}
複製代碼
await
並不像字面意義上程序運行到這裏就停下來啥也不幹等待Future
完成。而是馬上結束當前函數的執行並返回一個Future
。函數內剩餘代碼經過調度異步執行。
await
只能在async
函數中出現。async
函數中能夠出現多個await
,每碰見一個就返回一個Future
, 實際結果相似於用then
串起來的回調。async
函數也能夠沒有await
, 在函數體同步執行完畢之後返回一個Future
。使用async
和await
還有一個好處是咱們能夠用和同步代碼相同的try
/catch
機制來作異常處理。
foo() async {
try {
print('foo E');
var value = await bar();
print('foo X $value');
} catch (e) {
// 同步執行代碼中的異常和異步執行代碼的異常都會被捕獲
} finally {
}
}
複製代碼
在平常使用場景中,咱們一般利用async
,await
來異步處理IO,網絡請求,以及Flutter中的Platform channels通訊等耗時操做。
本文大體介紹了Flutter/Dart中的異步運行機制,從異步運行的基礎(Event Loop)開始,首先介紹了最原始的異步運行機制,直接調度回調函數;到Future
;再到 async
和await
。瞭解了Flutter/Dart中的異步運行機制是如何一步一步的進化而來的。對於一直從事Native開發,不太瞭解JavaScrip的同窗來說,這個異步機制和原生開發有很大的不一樣,須要多多動手練習,動腦思考才能適應。本文中介紹的相關知識點較爲粗淺,並無涉及dart:async
中關於Future
實現的源碼分析以及Stream
等不太經常使用的類。這些若是你們想了解一下的話我會另寫文章來介紹一下。