如何使用異步Future
什么是異步
如果你的程式中有兩個方法,這兩個方法桉順序執行,第一個方法執行需要五秒,如果是同步代碼,第二個方法會等待第一個方法執行完,才會被呼叫,
如果第一個方法是異步的,程式在執行第一個方法時,不會等待它執行結束,而是接著執行第二個方法,這樣第二個方法就無需在第一個方法執行完之后被呼叫,
在客戶端異步是非常有用的,如果你在初始化時有一個非常耗時,但又不需要它在ui畫面回應前執行完成的方法,你就可以使用異步,
Dart異步處理庫Future
了解了異步的概念后,我們來看一看如何在Dart中使用異步,
testFuture();
testFuture2();
Future testFuture() {
//下面是一個耗時三秒的任務
return Future.delayed(Duration(seconds: 3), () => print('異步方法'));
}
testFuture2() {
print("普通方法");
}
控制臺輸出
? 
將一個方法的回傳值宣告為Future這樣這個方法就是異步的了,
Future的構造方法
你也可以使用Future類的構造方法來使用異步
Future(() {
print('異步方法');
});
Future類的構造方法如下
普通的Future類構造
Future(FutureOr<T> computation())
創建一個延遲幾秒執行的Future duration引數來控制延遲多久
Future.delayed(Duration duration, [FutureOr<T> computation()])
通過微任務佇列處理的Future
Future.microtask(FutureOr<T> computation())
立即回傳結果的Future
Future.sync(FutureOr<T> computation())
Future.value([FutureOr<T>? value])
Future.error(Object error, [StackTrace? stackTrace])
構造方法演示
Future.delayed(Duration(seconds: 3), () => print('異步方法1'));
Future(() {
print('異步方法2');
});
Future.microtask(() => print('異步方法3'));
Future.sync(() => print('異步方法4'));
控制臺輸出如下

看到不同構造方法的執行順序,想必你已經對不同的構造方法有所了解
值得一提的是Future所有的構造方法回傳的都是Future物件,我們可以進行鏈式呼叫
Future的鏈式呼叫
當 future 執行完成后,then() 中的代碼會被執行,
Future(() {
print('異步方法');
}).then((value) => print('異步方法2'));
等待多個 Future
有時代碼邏輯需要呼叫多個異步函式, 并等待它們全部完成后再繼續執行, 使用 Future.wait() 靜態方法管理多個 Future 以及等待它們完成:
Future deleteLotsOfFiles() async => ...
Future copyLotsOfFiles() async => ...
Future checksumLotsOfOtherFiles() async => ...
Future.wait([
deleteLotsOfFiles(),
copyLotsOfFiles(),
checksumLotsOfOtherFiles(),
]);
簡化Future
使用async和awiat來簡化異步代碼
這樣宣告一個方法 它就是異步的了
testFuture5() async {
Future.delayed(Duration(seconds: 3), () => print('異步方法1'));
}
你也可以像寫同步代碼一樣使用異步,當在async宣告的方法中使用await時,async宣告的方法會等待await修飾的方法執行結束
testFuture2() {
print("普通方法");
}
testFuture5() async {
await Future.delayed(Duration(seconds: 3), () => print('異步方法1'));
}
test()async{
await testFuture5();
testFuture2();
}
控制臺輸出如下
如果你對以上的默寫代碼執行順序有所疑惑,不要著急,下面的內容會解答你的所有問題,
事件回圈基本概念
本文描述了Dart的事件回圈架構,您就可以撰寫出更好的更少問題的異步代碼,您將學習如何使用Future,并且能夠預測程式的執行順序,
如果你寫過UI代碼,你可能已經熟悉了事件回圈和事件佇列的概念,它們確保了圖形操作和事件(如滑鼠點擊)一次只處理一個,
事件回圈和佇列
事件回圈的作業是從事件佇列中獲取一個事件并處理它,只要佇列中有事件,就重復這兩個步驟,

佇列中的事件可能代表用戶輸入,檔案I / O通知,計時器等, 例如,下面是事件佇列的圖片,其中包含計時器和用戶輸入事件:

你可能在其他的語言中熟悉這些,現在我們來談談dart語言是如何實作的,
Dart的單執行緒
一旦一個Dart函式開始執行,它將繼續執行直到退出,換句話說,Dart函式不能被其他Dart代碼打斷,
如下圖所示,一個Dart程式開始執行的第一步是主isolate執行main()函式,當main()退出后,主isolate執行緒開始逐個處理程式事件佇列上的所有事件,

實際上,這有點過于簡化了,
dart的事件回圈和佇列
Dart應用程式的事件回圈帶有兩個佇列——事件佇列和微任務佇列,
事件佇列包含所有外部事件:I/O、滑鼠事件、繪圖事件、計時器、Dart isolate之間的通信,等等,
微任務佇列是必要的,因為事件處理代碼有時需要稍后完成一個任務,但在將控制權回傳到事件回圈之前,例如,當一個可觀察物件發生變化時,它將幾個突變變化組合在一起,并同步地報告它們,微任務佇列允許可觀察物件在DOM顯示不一致狀態之前報告這些突變變化,
事件佇列包含來自應用程式中的事件,微任務佇列只包含來自Dart核心代碼的事件,
如下圖所示,當main()函式退出時,事件回圈開始作業,首先,它以FIFO(先進先出)順序執行所有微任務,然后,它使事件佇列中的第一項出隊并處理,然后它重復這個回圈:執行所有微任務,然后處理事件佇列上的下一事件,一旦兩個佇列都為空并且不會再發生任何事件,應用程式的嵌入程式(如瀏覽器或測驗框架)就可以釋放應用程式,
注意:如果web應用程式的用戶關閉了它的視窗,那么web應用程式可能會在其事件佇列為空之前強行退出,

重要:當事件回圈正在執行微任務佇列中的任務時,事件佇列會卡住:應用程式無法繪制圖形、處理滑鼠點擊、對I/O做出反應等,
盡管可以預測任務執行的順序,但不能準確預測事件回圈何時將任務從佇列中移除,Dart事件處理系統基于單執行緒回圈;它不是基于任何型別的時間標準,例如,當您創建一個延遲的任務時,事件將在您指定的時間進入佇列,他還是要等待事件佇列中它之前的所有事件(包括微任務佇列中的每一個事件)全部執行完后,才能得到執行,(延時任務不是插隊,是在指定時間進入佇列)
提示:鏈式呼叫future指定任務順序
如果您的代碼有依賴關系,請以顯式的方式撰寫,顯式依賴關系幫助其他開發人員理解您的代碼,并且使您的程式能方便的重構,
下面是一個錯誤編碼方式的例子:
// 因為在設定變數和使用變數之間沒有明確的依賴關系,所以不好,
future.then((){...設定一個重要變數...),
Timer.run(() {...使用重要變數...}),
相反,像這樣寫代碼:
//更好,因為依賴關系是顯式的,
future.then(…設定一個重要的變數…)
then((_){…使用重要的變數…});
在使用該變數之前必須先設定它,(如果您希望即使出現錯誤也能執行代碼,那么可以使用whenComplete()而不是then(),)
如果使用變數需要時間并且可以在以后完成,請考慮將代碼放在新的Future中:
//可能更好:顯式依賴加上延遲執行,
future.then(…設定一個重要的變數…)
then((_) {new Future((){…使用重要的變數…})});
使用新的Future使事件回圈有機會處理事件佇列中的其他事件,下一節將詳細介紹延遲運行的調度代碼,
如何安排任務
當您需要指定一些需要延遲執行的代碼時,可以使用dart:async庫提供的以下API:
Future類,它將一個專案添加到事件佇列的末尾,
頂級的scheduleMicrotask()函式,它將一個專案添加到微任務佇列的末尾,
使用這些api的示例在下一節中,事件佇列:new Future()和微任務佇列:scheduleMicrotask()
使用適當的佇列(通常是事件佇列)
盡可能的在事件佇列上調度任務,使用Future,使用事件佇列有助于保持微任務佇列較短,減少微任務佇列影響事件佇列的可能,
如果一個任務需要在處理任何來自事件佇列的事件之前完成,那么你通常應該先執行該函式,如果不能先執行,那么使用 scheduleMicrotask()將這個任務添加到微任務佇列中,

事件佇列: new Future()
要在事件佇列上調度任務,可以使用new Future()或new Future.delayed(),這是dart:async庫中定義的兩個Future的建構式,
注意:您也可以使用Timer安排任務,但是如果Timer任務中發生任何未捕獲的例外,您的應用程式將退出, 相反,我們建議使用Future,它建立在Timer之上,并增加了諸如檢測任務完成和對錯誤進行回應的功能,
要立即將一個事件放到事件佇列中,使用new Future():
//在事件佇列中添加任務,
new Future((){
/……代碼就在這里……
});
您可以添加對then()或whenComplete()的呼叫,以便在新的Future完成后立即執行一些代碼,例如,當new Future的任務離開佇列時,以下代碼輸出“42”:
new Future(() => 21)
.then((v) => v*2)
.then((v) => print(v));
使用new Future.delayed()在一段時間后在佇列中加入一個事件:
// 一段時間之后,將事件加入佇列
new Future.delayed(const Duration(seconds:1), () {
// ...代碼在這里...
});
盡管前面的示例在一秒后將任務添加到事件佇列中,但該任務只有在主isolate空閑、微任務佇列為空以及之前在事件佇列中入隊的任務全部執行完后才能執行,例如,如果main()函式或事件處理程式正在運行一個復雜的計算,則任務只有在該計算完成后才能執行,在這種情況下,延遲可能遠不止一秒,
關于future的重要細節:
1 傳遞給Future的then()方法的函式在Future完成時立即執行,(函式沒有進入佇列,只是被呼叫了)
2 如果Future在呼叫then()之前已經完成,則將一個任務添加到微任務佇列,然后該任務執行傳遞給then()的函式,
3 Future()和Future.delayed()建構式不會立即完成; 他們將一個專案添加到事件佇列,
4 value()建構式在微任務中完成,類似于#2
5 Future.sync()建構式立即執行其函式引數,并且(除非該函式回傳Future,如果回傳future代碼會進入事件佇列)在微任務中完成,類似于#2,(Future.sync(FutureOr
微任務佇列:scheduleMicrotask()
async庫將scheduleMicrotask()定義為一個頂級函式,你可以像這樣呼叫scheduleMicrotask():
scheduleMicrotask(() {
// ...代碼在這里...
});
dart2.9會將第一次呼叫scheduleMicrotask()時,將此代碼插入事件佇列的第一位
向微任務佇列添加任務的一種方法是在已經完成的Future上呼叫then(),有關更多資訊,請參閱前一節
必要時使用isolates 或workers
現在您已經閱讀了關于調度任務的所有內容,讓我們測驗一下您的理解,
請記住,您不應該依賴Dart的事件佇列實作來指定任務順序, 實作可能會發生變化,Future的then()和whenComplete()方法是更好的選擇, 不過,如果您能正確回答下面這些問題,你學會了,
練習
Question #1
這個示例列印出什么?
import 'dart:async';
void main() {
print('main #1 of 2');
scheduleMicrotask(() => print('microtask #1 of 2'));
new Future.delayed(new Duration(seconds:1),
() => print('future #1 (delayed)'));
new Future(() => print('future #2 of 3'));
new Future(() => print('future #3 of 3'));
scheduleMicrotask(() => print('microtask #2 of 2'));
print('main #2 of 2');
}
答案
main #1 of 2
main #2 of 2
microtask #1 of 2
microtask #2 of 2
future #2 of 3
future #3 of 3
future #1 (delayed)
這個順序應該你能預料到的,因為示例代碼分三批執行:
1 main()函式中的代碼
2 微任務佇列中的任務(scheduleMicrotask())
3 事件佇列中的任務(new Future()或new Future.delayed())
請記住,main()函式中的所有呼叫都是從頭到尾同步執行的,首先main()呼叫print(),然后呼叫scheduleMicrotask(),再呼叫new Future.delayed(),然后呼叫new Future(),以此類推,只有回呼--作為 scheduleMicrotask()、new Future.delayed()和new Future()的引數代碼才會在后面的時間執行
Question #2
這里有一個更復雜的例子,如果您能夠正確地預測這段代碼的輸出,就會得到一個閃亮的星星,
import 'dart:async';
void main() {
print('main #1 of 2');
scheduleMicrotask(() => print('microtask #1 of 3'));
new Future.delayed(new Duration(seconds:1),
() => print('future #1 (delayed)'));
new Future(() => print('future #2 of 4'))
.then((_) => print('future #2a'))
.then((_) {
print('future #2b');
scheduleMicrotask(() => print('microtask #0 (from future #2b)'));
})
.then((_) => print('future #2c'));
scheduleMicrotask(() => print('microtask #2 of 3'));
new Future(() => print('future #3 of 4'))
.then((_) => new Future(
() => print('future #3a (a new future)')))
.then((_) => print('future #3b'));
new Future(() => print('future #4 of 4'));
scheduleMicrotask(() => print('microtask #3 of 3'));
print('main #2 of 2');
}

dart程式會在第一次創建微任務佇列時,將創建微任務佇列的代碼插入到事件佇列的第一位,相當于插隊,
總結
你現在應該了解Dart的事件回圈以及dart如何安排任務,以下是Dart中事件回圈的一些主要概念:
Dart應用程式的事件回圈使用兩個佇列執行任務:事件佇列和微任務佇列,
事件佇列有來自Dart(futures、計時器、isolate messages)和系統(用戶操作、I/O等)的事件,
目前,微任務佇列只有來自Dart核心代碼的事件,如果你想讓你的代碼進入微任務佇列執行,使用scheduleMicrotask(),
事件回圈在退出佇列并處理事件佇列上的下一項之前先清空微任務佇列,
一旦兩個佇列都為空,應用程式就完成了它的作業,并且(取決于它的嵌入程式)可以退出,
main()函式和來自微任務和事件佇列的所有專案都運行在Dart應用程式的主isolates 上,
當你安排一項事件時,遵循以下規則:
如果可能,將其放在事件佇列中(使用new Future()或new Future.delayed()),
使用Future的then()或whenComplete()方法指定任務順序,
為了避免耗盡事件回圈,請保持微任務佇列盡可能短,
為了保持應用程式的回應性,避免在任何一個事件回圈中執行計算密集型任務,
要執行計算密集型任務,請創建額外的isolates 或者 workers,
參考文章:
https://dart.cn/articles/archive/event-loop#:~:text=A Dart app has a,queue and the microtask queue.&text=First, it executes any microtasks,item on the event queue.
https://api.dart.dev/stable/2.10.4/dart-async/Future-class.html
https://www.dartcn.com/guides/libraries/library-tour#future
文章中所有的測驗異步的代碼都在作者的github上,https://github.com/jack0-0wu/flutter_demo,
如果你對Dart flutter 計算機基礎感興趣可以關注作者,持續分享優質文章,
坐而論道不如起而行之
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/234200.html
標籤:其他
