Flutter篇之你真的會使用Future嗎?

入魔的冬瓜發表於2019-07-19

最近用Flutter寫了個簡單的“爬蟲”的東西,就是要不斷地請求某些介面,然後讀取資料就可以了。之前只是會簡單地await和async來試用Future,後面發現只會簡單的這種方式不行,於是去學習了Future的其他用法,還是有點收穫的,把Future的用法都學了一下,這裡做個筆記。

哈哈,別問我為什麼沒用python去搞。。。剛好用的電腦沒裝環境並且python的API沒那麼熟,就有了個想法,反正都能實現,就用Flutter玩一下吧,可能後面還可以搞個視覺化介面玩玩。

Dart的訊息迴圈機制

推薦文章:Dart與訊息迴圈機制

這裡簡單說下Dart的事件迴圈,更加具體的話,可以看推薦的這篇文章。

Flutter篇之你真的會使用Future嗎?

類似Android的Handler/Looper機制,Dart也有相應的訊息迴圈機制。

  • 一個Dart應用中有一個訊息迴圈和兩個佇列,一個是event佇列,一個是microtask佇列。
  • 優先順序問題:microtask佇列的優先順序是最高的,當所有microtask佇列執行完之後才會從event佇列中讀取事件進行執行。
  • microtask佇列中的event,
  • event佇列包含所有的外來事件:I/O,mouse events,drawing events,timers,isolate之間的message等。
  • 一般Future建立的事件是屬於event佇列的(利用Future.microtask方法例外),所以建立一個Future後,會插入到event佇列中,順序執行。

Future的介紹

在寫程式的過程中,肯定會有一部分比較耗時程式碼是需要非同步執行的。比如網路操作,我們需要非同步去請求資料,並且還需要處理請求成功和請求失敗的兩種情況。

在Flutter中,使用Future來執行耗時操作,表示在未來會返回某個值,並可以使用then()方法和catchError()來註冊callback來監聽Future的處理結果。

如下面所示,原始碼中對Future的解釋,以及簡單的一個例子。then()方法和catchError()返回的其實也是一個Future型別物件,所以可以寫成鏈式呼叫的形式。

An object representing a delayed computation.
Future<int> future = getFuture();
future.then((value) => handleValue(value))
      .catchError((error) => handleError(error));
複製程式碼

Flutter的狀態

一個Future物件會有以下兩種狀態

  • pending:表示Future物件的計算過程仍在執行中,這個時候還沒有可以用的result。
  • completed:表示Future物件已經計算結束了,可能會有兩種情況,一種是正確的結果,一種是失敗的結果。

建立Future

注意:首先Future是個泛型類,可以指定型別。如果沒有指定相應型別的話,預設返回是Future型別的。

Future createFuture()async{
  return 1;
}
var future=createFuture();
print(future.runtimeType);。//輸出結果為Future<dynamic>
複製程式碼

Future常用的建立有以下幾種方式。

Flutter篇之你真的會使用Future嗎?

基本的用法

factory Future(FutureOr computation())

Future的構造方法,建立一個基本的Future

var future = Future(() {
  print("哈哈哈");
});
print("111");
//111
//哈哈哈
複製程式碼

你會發現,結果是列印“哈哈哈”字串是在後面才輸出的,原因是預設future是非同步執行的。如果改成下面的程式碼,在future前面加上await關鍵字,則會先列印“哈哈哈”字串。await會等待直到future執行結束後,才繼續執行後面的程式碼。

這個跟上面的事件迴圈是有關係的,轉發大佬的解釋:main方法中的普通程式碼都是同步執行的,所以肯定是main列印先全部列印出來,等main方法結束後會開始檢查microtask中是否有任務,若有則執行,執行完繼續檢查microtask,直到microtask列隊為空。所以接著列印的應該是microtask的列印。最後會去執行event佇列。

var future = await Future(() {
  print("哈哈哈");
});
print("111");
//哈哈哈
//111
複製程式碼

Future.value()

建立一個返回指定value值的Future

var future=Future.value(1);
print(future);
//Instance of 'Future<int>'
複製程式碼

Future.delayed()

建立一個延遲執行的future。 例如下面的例子,利用Future延遲兩秒後可以列印出字串。

var futureDelayed = Future.delayed(Duration(seconds: 2), () {
  print("Future.delayed");
  return 2;
});
複製程式碼

高階的用法

Future.foreach(Iterable elements, FutureOr action(T element))

根據某個集合,建立一系列的Future,並且會按順序執行這些Future。 比如下面的例子,根據{1,2,3}建立3個延遲對應秒數的Future。執行結果為1秒後列印1,再過2秒列印2,再過3秒列印3,總時間為6秒。

Future.forEach({1,2,3}, (num){
  return Future.delayed(Duration(seconds: num),(){print(num);});
});
複製程式碼

Future.wait ( Iterable<Future> futures,{bool eagerError: false, void cleanUp(T successValue)})

用來等待多個future完成,並收集它們的結果。那這樣的結果就有兩種情況了:

  • 如果所有future都有正常結果返回:則future的返回結果是所有指定future的結果的集合
  • 如果其中一個future有error返回:則future的返回結果是第一個error的值。

比如下面的例子,也是建立3個延遲對應秒數的Future。結果是總時間過了3秒後,才輸出[1, 2, 3]的結果。可以與上面的例子對比一下,一個是順序執行多個Future,一個是非同步執行多個Future。

var future1 = new Future.delayed(new Duration(seconds: 1), () => 1);
var future2 =
    new Future.delayed(new Duration(seconds: 2), () => 2);
var future3 = new Future.delayed(new Duration(seconds: 3), () => 3);
Future.wait({future1,future2,future3}).then(print).catchError(print);
//執行結果: [1, 2, 3]
複製程式碼

將future2和future3改為有error丟擲,則future.wait的結果是這個future2的error值。

var future1 = new Future.delayed(new Duration(seconds: 1), () => 1);
var future2 =new Future.delayed(new Duration(seconds: 2), () => throw “throw error2");
var future3 = new Future.delayed(new Duration(seconds: 3), () => throw “throw error3");
Future.wait({future1,future2,future3}).then(print).catchError(print);
//執行結果: throw error2
複製程式碼

Future.any(futures)

返回的是第一個執行完成的future的結果,不會管這個結果是正確的還是error的。(感覺這個的使用場景沒幾個的樣子,想不到有什麼場景要用到這個。)

例如下面的例子,使用Future.delayed()延遲建立了三個不同的Future,第一個完成返回的是延遲1秒的那個future的結果。

 Future
      .any([1, 2, 5].map(
          (delay) => new Future.delayed(new Duration(seconds: delay), () => delay)))
      .then(print)
      .catchError(print);
複製程式碼

Future.doWhile()

類似java中doWhile的用法,重複性地執行某一個動作,直到返回false或者Future,退出迴圈。

使用場景:適用於一些需要遞迴操作的場景。

比如我就是要爬某個平臺的商品分類的資料,那每個分類下面又有相應的子分類。那我就得拿對應父級分類的catId去請求介面,拿到這個分類下面的所有子分類。同樣的,對所有的子分類,也要進行一樣的操作,遞迴直到這個分類是一個葉子節點,也就是該節點下面沒有子分類(有個leaf為true的欄位)。哈哈,程式碼我就沒貼了,其實也就是一個遞迴操作,直到滿足條件退出future。

例如下面的例子,生成一個隨機數進行等待,直到十秒之後,操作結束。

void futureDoWhile(){
  var random = new Random();
  var totalDelay = 0;
  Future
      .doWhile(() {
    if (totalDelay > 10) {
      print('total delay: $totalDelay seconds');
      return false;
    }
    var delay = random.nextInt(5) + 1;
    totalDelay += delay;
    return new Future.delayed(new Duration(seconds: delay), () {
      print('waited $delay seconds');
      return true;
    });
  })
      .then(print)
      .catchError(print);
}
//輸出結果:
I/flutter (11113): waited 5 seconds
I/flutter (11113): waited 1 seconds
I/flutter (11113): waited 3 seconds
I/flutter (11113): waited 2 seconds
I/flutter (11113): total delay: 12 seconds
I/flutter (11113): null
複製程式碼

Future.microtask(FutureOr computation())

建立一個在microtask佇列執行的future。

在上面講過,microtask佇列的優先順序是比event佇列高的,而一般future是在event佇列執行的,所以Future.microtask建立的future會優先於其他future進行執行。

例如下面的程式碼,

Future((){
  print("Future event 1");
});
Future((){
  print("Future event 2");
});
Future.microtask((){
 print("microtask event");
});
//輸出結果
//microtask event
//Future event 1
//Future event 2
複製程式碼

處理Future的結果

Flutter提供了下面三個方法,讓我們來註冊回撥,來監聽處理Future的結果。

Future<R> then<R>(FutureOr<R> onValue(T value), {Function onError});
Future<T> catchError(Function onError, {bool test(Object error)});
Future<T> whenComplete(FutureOr action());
複製程式碼

Future.then()

用於在Future完成的時候新增回撥。

注意:then()的返回值也是一個Future物件。所以我們可以使用鏈式的方法去使用Future,將前一個Future的輸出結果作為後一個Future的輸入,可以寫成鏈式呼叫。

例如下面的程式碼,將前面的future結果作為後面Future的輸入。

Future.value(1).then((value) {
  return Future.value(value + 2);
}).then((value) {
  return Future.value(value + 3);
}).then(print);
//列印結果為6
複製程式碼

Future.cathcError()

註冊一個回撥,來處理有error的Future

new Future.error('boom!').catchError(print);
複製程式碼

與then方法中的onError的區別:then方法裡面也有個onError的引數,也可以用來處理錯誤的Future。

兩者的區別,onError只能處理當前的錯誤Future,而不能處理其他有錯誤的Future。catchError可以捕獲到Future鏈中丟擲的所有錯誤。

所以通常的做法是使用catchError來捕捉Future中的所有錯誤,不建議使用then方法中的onError方法。不然每個future的then方法都要加上onError回撥的話,就比較麻煩了,而且程式碼看起來也是有點亂。

下面是兩個捕捉錯誤的例子。

在那個丟擲錯誤的future的then方法裡新增onError回撥的話,onError會優先被呼叫

Future.value(1).then((value) {
  return Future.value(value + 2);
}).then((value) {
  throw "error";
}).then(print, onError: (e) {
  print("onError find a error");
}).catchError((e) {
  print("catchError find a error");
});
//輸出結果為onError find a error
複製程式碼

使用catchError來監聽鏈式呼叫Future裡面丟擲來的錯誤。

Future.value(1).then((value) {
  throw "error";
}).then((value) {
  return Future.value(3);
}).then(print).then((value) {
}).catchError((e) {
  print("catchError find a error");
});
//輸出結果為catchError find a error"
複製程式碼

Future.whenComplete

類似Java中的finally,Future.whenComplete總是在Future完成後呼叫,不管Future的結果是正確的還是錯誤的。

注意:Future.whenComplete的返回值也是一個Future物件。

Future.delayed(Duration(seconds: 3),(){
  print("哈哈");
}).whenComplete((){
  print("complete");
});
//哈哈
//complete
複製程式碼

最常用的async和await

哈哈,上面說了一些Future的用法,其實有些可能並不會很常用,知道有這個用法就行了。

有個問題,如果有多個Future連結在一起的話,靠,程式碼可能會變得難以閱讀。

所以,可以使用async和await來使用Future,使程式碼看起來像是同步的程式碼,但實際上它們還是非同步執行的。

最常用的還是async和await來使用Future。

注意:await只能在async函式出現。 async函式,返回值是一個Future物件,如果沒有返回Future物件,會自動將返回值包裝成Future物件。 捕捉錯誤,一般是使用try/catch機制來做異常處理。 await 一個future,可以拿到Future的結果,直到拿到結果,才執行下一步的程式碼。

比如下面的例子,建立一個延遲3秒並返回結果為1的Future,使用await來獲取到future的值。

void main() async {
  var future = new Future.delayed(new Duration(seconds: 3), () {
    return 1;
  });
  var result = await future;
  print(result + 1);
}
 //輸出為2
複製程式碼

下面的程式碼結構,跟java一樣的寫法,使用try-catch-finally來捕捉錯誤。

try {
  var result1 = await Future.value(1);
  print(result1);//輸出1
  var result2 = await Future.value(2);
  print(result2);//輸出2
} catch (e) {
    //捕捉錯誤
} finally {
  
}
複製程式碼

總結

  • 建立Future的幾種方法
  • 用then、catchError、whenComplete來處理Future的結果
  • async和await的使用

下一步

多學習多整理。靠,又懶了好長一段時間了。。。

相關文章