flutter實戰5:非同步async、await和Future的使用技巧

燃燒的魚丸發表於2018-04-15

由於前面的HTTP請求用到了非同步操作,不少小夥伴都被這個問題折了下腰,今天總結分享下實戰成果。Dart是一個單執行緒的語言,遇到有延遲的運算(比如IO操作、延時執行)時,執行緒中按順序執行的運算就會阻塞,使用者就會感覺到卡頓,於是通常用非同步處理來解決這個問題。當遇到有需要延遲的運算(async)時,將其放入到延遲運算的佇列(await)中去,把不需要延遲運算的部分先執行掉,最後再來處理延遲運算的部分。

async和await

首先看一個案例:

  //HTTP的get請求返回值為Future<String>型別,即其返回值未來是一個String型別的值
  getData() async {    //async關鍵字宣告該函式內部有程式碼需要延遲執行
    return await http.get(Uri.encodeFull(url), headers: {"Accept": "application/json"}); //await關鍵字宣告運算為延遲執行,然後return運算結果
  }
複製程式碼

然後我們呼叫這個函式,想獲取其結果:

  String data = getData();
複製程式碼

在書寫時,在IDE中這個程式碼是沒有問題的,但是當我們執行這段程式碼時,就報錯了:

Future型別不匹配的錯誤

為什麼呢?因為dataString型別,而函式getData()是一個非同步操作函式,其返回值是一個await延遲執行的結果。在Dart中,有await標記的運算,其結果值都是一個Future物件,Future不是String型別,所以就報錯了。

那如果這樣的話,我們就沒法獲取到延遲執行的結果了?當然可以,Dart規定有async標記的函式,只能由await來呼叫,比如這樣:

String data = await getData();
複製程式碼

但是要使用await,必須在有async標記的函式中執行,否則這個await會報錯:

await用法不正確

於是,我們要為這個給data賦值的語句加一個async函式的包裝:

String data;
setData() async {
  data = await getData();    //getData()延遲執行後賦值給data
}
複製程式碼

上面這種方法一般用於呼叫封裝好的非同步介面,比如getData()被封裝到了其他dart檔案,通過使用async函式對其調取使用

再或者,我們去掉async函式的包裝,在getData()中直接完成data變數的賦值:

String data;
getData() async {
  data = await http.get(Uri.encodeFull(url), headers: {"Accept": "application/json"});     //延遲執行後賦值給data
}
複製程式碼

這樣,data就獲取到HTTP請求的資料了。就這樣就完了?是滴,只要記住兩點:

  • await關鍵字必須在async函式內部使用
  • 呼叫async函式必須使用await關鍵字

PS:await關鍵字真的很形象,等一等的意思,就是說,既然你執行的時候都要等一等,那我呼叫的時候也等一等吧

Future簡單科普

前面個講到過,直接return await ...的時候,實際上返回的是一個延遲計算的Future物件,這個Future物件是Dart內建的,有自己的佇列策略,我們就來聊聊這個Future

先囉嗦一些關於Dart線上程方面的知識。

Dart是基於單執行緒模型的語言。在Dart也有自己的程式(或者叫執行緒)機制,名叫isolate。APP的啟動入口main函式就是一個isolate。玩家也可以通過引入import 'dart:isolate'建立自己的isolate,對多核CPU的特性來說,多個isolate可以顯著提高運算效率,當然也要適當控制isolate的數量,不應濫用,否則走火入魔自廢武功。有一個很重要的點,Dart中isolate之間無法直接共享記憶體,不同的isolate之間只能通過isolate API進行通訊,當然本篇的重點在於Future,不展開講isolate,心急的小夥伴可以參考官方閱讀理解或者參考大神tain335人肉翻譯

Dart執行緒中有一個訊息迴圈機制(event loop)和兩個佇列(event queuemicrotask queue)。

  • event queue包含所有外來的事件:I/O,mouse events,drawing events,timers,isolate之間的message等。任意isolate中新增的event(I/O,mouse events,drawing events,timers,isolate的message)都會放入event queue中排隊等待執行,好比機場的公共排隊大廳。

  • microtask queue只在當前isolate的任務佇列中排隊,優先順序高於event queue,好比機場裡的某個VIP候機室,總是VIP使用者先登機了,才開放公共排隊入口。

如果在event中插入microtask,當前event執行完畢即可插隊執行microtask。如果沒有microtask,就沒辦法插隊了,也就是說,microtask queue的存在為Dart提供了給任務佇列插隊的解決方案。

main方法執行完畢退出後,event loop就會以FIFO(先進先出)的順序執行microtask,當所有microtask執行完後它會從event queue中取事件並執行。如此反覆,直到兩個佇列都為空,如下流程圖:

event queue和microtask queue

注意:當事件迴圈正在處理microtask的時候,event queue會被堵塞。這時候app就無法進行UI繪製,響應滑鼠事件和I/O等事件。胡亂插隊也是有代價的~

雖然你可以預測任務執行的順序,但你無法準確的預測到事件迴圈何時會處理你期望的任務。例如當你建立一個延時1s的任務,但在排在你之前的任務結束前事件迴圈是不會處理這個延時任務的,也就是或任務執行可能是大於1s的。

OK,瞭解以上資訊之後,再來回到Future,小夥伴可能已經被繞暈了。

Future就是event,很多Flutter內建的元件比如前幾篇用到的Http(http請求控制元件)的get函式、RefreshIndicator(下拉手勢重新整理控制元件)的onRefresh函式都是event。每一個被await標記的控制程式碼也是一個event,每建立一個Future就會把這個Future扔進event queue中排隊等候安檢~

什麼?那microtask呢?當然不會忘了這個,scheduleMicrotask,用法和Future基本一樣。

為什麼要用Future?

前面講到,用asyncawait組合,即可向event queue中插入event實現非同步操作,好像Future的存在有些多餘的感覺,剛開始我本人也有這樣的疑惑,且往下看。

當定義Flutter函式時,還可以指定其執行結果返回值的型別,以提高程式碼的可讀性:

//定義了返回結果值為String型別
Future<String> getDatas(String category) async {
    var request = await _httpClient.getUrl(Uri.parse(url));  
    var response = await request.close();
    return await response.transform(utf8.decoder).join();
}

run() async{
    int data = await getDatas('keji');    //因為型別不匹配,IDE會報錯
}
複製程式碼

Future最主要的功能就是提供了鏈式呼叫。熟悉ES6語法的小夥伴樂開了花,鏈式呼叫解決兩大問題:明確程式碼執行的依賴關係和實現異常捕獲。WTF?還不明白?且看下面這些案例:

//案例1
funA() async{
  ...set an important variable...
}

funB() async{
  await funA();
  ...use the important variable...
}

main() async {
  funB();   
}
//如果要想先執行funA再執行funB,必須在funB中await funA();
//funB的程式碼與funA耦合,將來如果funA廢掉或者改動,funB中還需要經過修改以適配變更。

//案例2
funA() async{
  try{
     ...set an important variable...
  }catch(e){
    do sth...
  }finally{
    do sth. else...
  }
}

funB() async{
  try{
     ...use the important variable...
  }catch(e){
    do sth...
  }finally{
    do sth. else...
  }
}

main() async {
  await funA();
  await funB();
}
//沒有明確體現出設定變數和使用變數之間的依賴關係,其他開發者難以理解你的程式碼邏輯,程式碼維護困難
//並且如果為了防止funA()或者funB()因發生異常導致程式崩潰
//要到funA()或者funB()中分別加入`try`、`catch`、`finally`
複製程式碼

為了解決上面的問題,Future提供了一套非常簡潔的解決方案:

//案例3
 funA(){
  ...set an important variable...    //設定變數
}

funB(){
  ...use the important variable...   //使用變數
}
main(){
  new Future.then(funA()).then(funB());   // 明確表現出了後者依賴前者設定的變數值
 
  new Future.then(funA()).then((_) {new Future(funB())});    //還可以這樣用

  //鏈式呼叫,捕獲異常
  new Future.then(funA(),onError: (e) { handleError(e); }).then(funB(),onError: (e) { handleError(e); })  
}
複製程式碼

案例3的玩法是asyncawait無法企及的,因此掌握Future還是很有必要滴。當然了,Future的玩法不僅僅侷限於案例3,還有很多有趣的玩法,包括和microtask物件scheduleMicrotask配合使用,我這裡就不一一介紹了,大家參考大神tain335人肉翻譯或者官網閱讀理解吧。

總結

Dart的isolate中加入了event queuemicrotask queue後,有了一點協程的感覺,或許這就是Flutter為啥在效能上敢和原生開發叫板的原因之一吧。本篇的內容比較抽象,如果還是有不明白的小夥伴,歡迎留言提問,我儘量回答,哈哈哈,就醬,歡迎加入到Flutter圈子flutter 中文社群(官方QQ群:338252156),群裡有前後端及全棧各路大神鎮場子,加入進來沒事就寫寫APP掙點外快(這個真的有),順便翻譯翻譯官方英文原稿拉一票粉絲,一舉多得何樂而不為呢。

相關文章