Flutter非同步程式設計-Future

熊喵先生發表於2021-02-28

Future可以說是在Dart非同步程式設計中隨處可見,比如一個網路請求返回的是Future物件,或者訪問一個SharedPreferences返回一個Future物件等等。非同步操作可以讓你的程式在等待一個操作完成時繼續處理其它的工作。而Dart 使用 Future 物件來表示非同步操作的結果。我們通過前面文章都知道Dart是一個單執行緒模型的語言,所以遇到延遲的計算一般都需要採用非同步方式來實現,那麼 Future 就是代表這個非同步返回的結果。

1. 複習幾個概念

1.1 事件迴圈EventLoop

Dart的EventLoop事件迴圈和Javascript很類似,同樣迴圈中擁有兩個FIFO佇列: 一個是事件佇列(Event Queue),另一個就是微任務佇列(MicroTask Queue)。

  • Event Queue主要包含IO、手勢、繪製、定時器(Timer)、Stream流以及本文所講Future等
  • MicroTask Queue主要包含Dart內部的微任務(內部非常短暫操作),一般是通過 scheduleMicroTask 方法實現排程,它的優先順序比Event Queue要高

1.2 事件迴圈執行的流程

  • 1、初始化兩個佇列,分別是事件佇列和微任務佇列
  • 2、執行 main 方法
  • 3、啟動事件迴圈EventLoop

注意:當事件迴圈正在處理microtask的時候,event queue會被堵塞。

2. 為什麼需要Future

我們都知道Dart是單執行緒模型語言,如果需要執行一些延遲的操作或者IO操作,預設採用單執行緒同步的方式就有可能會造成主isolate阻塞,從而導致渲染卡頓。基於這一點我們需要實現單執行緒的非同步方式,基於Dart內建的非阻塞API實現,那麼其中Future就是非同步中非常重要的角色。Future表示非同步返回的結果,當執行一個非同步延遲的計算時候,首先會返回一個Future結果,後續程式碼可以繼續執行不會阻塞主isolate,當Future中的計算結果到達時,如果註冊了 then 函式回撥,對於返回成功的回撥就會拿到最終計算的值,對於返回失敗的回撥就會拿到一個異常資訊

可能很多人有點疑惑,實現非同步使用isolate就可以了,為什麼還需要Future? 關於這點前面文章也有說過,Future相對於isolate來說更輕量級,而且建立過多的isolate對系統資源也是非常大的開銷。熟悉isolate原始碼小夥伴就知道,實際上isolate對映對應作業系統的OSThread. 所以對於一些延遲性不高的還是建議使用Future來替代isolate.

可能很多人還是有疑惑,關於實現非同步Dart中的async和await也能實現非同步,為什麼還需要Future? Future相對於async, await的最大優勢在於它提供了強大的鏈式呼叫,鏈式呼叫優勢在於可以明確程式碼執行前後依賴關係以及實現異常的捕獲。 一起來看個常見的場景例子,比如需要請求book詳情時,需要先請求拿到對應book id, 也就是兩個請求有前後依賴關係的,這時候如果使用async,await可能就不如future來的靈活。

  • 使用async和await實現:
_fetchBookId() async {
  //request book id
}

_fetchBookDetail() async {
  //需要在_fetchBookDetail內部去await取到bookId,所以要想先執行_fetchBookId再執行_fetchBookDetail,
  //必須在_fetchBookDetail中await _fetchBookId(),這樣就會存在一個問題,_fetchBookDetail內部耦合_fetchBookId
  //一旦_fetchBookId有所改動,在_fetchBookDetail內部也要做相應的修改
  var bookId = await _fetchBookId();
  //get bookId then request book detail
}

void main() async {
   var bookDetail = await _fetchBookDetail();// 最後在main函式中請求_fetchBookDetail
}

//還有就是異常捕獲
_fetchDataA() async {
  try {
    //request data
  } on Exception{
    // do sth
  } finally {
    // do sth
  }
}

_fetchDataB() async {
  try {
    //request data
  } on Exception{
    // do sth
  } finally {
    // do sth
  }
}
void main() async {
  //防止異常崩潰需要在每個方法內部都要新增try-catch捕獲
  var resultA = await _fetchDataA();
  var resultB = await _fetchDataB();
}
複製程式碼
  • Future的實現
_fetchBookId() {
  //request book id
}

_fetchBookDetail() {
  //get bookId then request book detail
}

void main() {
   Future(_fetchBookId()).then((bookId) => _fetchBookDetail());
  //或者下面這種方式
   Future(_fetchBookId()).then((bookId) => Future(_fetchBookDetail()));
}

//捕獲異常的實現
_fetchBookId() {
  //request book id
}

_fetchBookDetail() {
  //get bookId then request book detail
}

void main() {
  Future(_fetchBookId())
      .catchError((e) => '_fetchBookId is error $e')
      .then((bookId) => _fetchBookDetail())
      .catchError((e) => '_fetchBookDetail is error $e');
}

複製程式碼

總結一下,為什麼需要Future的三個點:

  • 在Dart單執行緒模型,Future作為非同步的返回結果是Dart非同步中不可或缺的一部分。
  • 在大部分Dart或者Flutter業務場景下,Future相比isolate實現非同步更加輕量級,更加高效。
  • 在一些特殊場景下,Future相比async, await在鏈式呼叫上更有優勢。

3. 什麼是Future

3.1 官方描述

用專業術語來說future 是 Future<T> 類的物件,其表示一個 T 型別的非同步操作結果。如果非同步操作不需要結果,則 future 的型別可為 Future<void>。當一個返回 future 物件的函式被呼叫時,會發生兩件事:

  • 將函式操作列入佇列等待執行並返回一個未完成的 Future 物件。
  • 不久後當函式操作執行完成,Future 物件變為完成並攜帶一個值或一個錯誤。

3.2 個人理解

其實我們可以把Future理解為一個裝有資料的“盒子”,一開始執行一個非同步請求會返回一個Future“盒子”,然後就接著繼續下面的其他程式碼。等到過了一會非同步請求結果返回時,這個Future“盒子”就會開啟,裡面就裝著請求結果的值或者是請求異常。那麼這個Future就會有三種狀態分別是: 未完成的狀態(Uncompleted), “盒子”處於關閉狀態;完成帶有值的狀態(Completed with a value), “盒子”開啟並且正常返回結果狀態;完成帶有異常的狀態(Completed with a error), “盒子”開啟並且失敗返回異常狀態;

下面用一個Flutter的例子來結合EventLoop理解下Future的過程,這裡有個按鈕點選後會去請求一張網路圖片,然後把這張圖片顯示出來。

RaisedButton(
  onPressed: () {
    final myFuture = http.get('https://my.image.url');//返回一個Future物件
    myFuture.then((resp) {//註冊then事件
      setImage(resp);
    });
  },
  child: Text('Click me!'),
)
複製程式碼
  • 首先,當點選 RaisedButton 的時候,會傳遞一個 Tap 事件到事件佇列中(因為系統手勢屬於Event Queue),然後 Tap 事件交由給事件迴圈EventLoop處理。

image.png

  • 然後,事件迴圈會處理 Tap 事件最終會觸發執行 onPressed 方法,隨即就會利用http庫發出一個網路請求,並且就會返回一個Future, 這時候你就拿到這個資料“盒子”,但是這時候它的狀態是關閉的也就是uncompleted狀態。然後再用 then 註冊當資料“盒子”開啟時候的回撥。這時候 onPressed 方法就執行完畢了,然後就是一直等著,等著HTTP請求返回的圖片資料,這時候整個事件迴圈不斷執行在處理其他的事件。

image.png

  • 最終,HTTP請求的圖片資料到達了,這時候Future就會把真正的圖片資料裝入到“盒子”中並且把盒子開啟,然後註冊的 then 回撥方法就會被觸發,拿到圖片資料並把圖片顯示出來。

image.png

4. Future的狀態

通過上面理解Future總共有3種狀態分別是: 未完成的狀態(Uncompleted), “盒子”處於關閉狀態;完成帶有值的狀態(Completed with a value), “盒子”開啟並且正常返回結果狀態;完成帶有異常的狀態(Completed with a error), “盒子”開啟並且失敗返回異常狀態. 

實際上從Future原始碼角度分析,總共有5種狀態:

  • __stateIncomplete : _初始未完成狀態,等待一個結果
  • __statePendingComplete: _Pending等待完成狀態, 表示Future物件的計算過程仍在執行中,這個時候還沒有可以用的result.
  • __stateChained: _連結狀態(一般出現於當前Future與其他Future連結在一起時,其他Future的result就變成當前Future的result)
  • __stateValue: _完成帶有值的狀態
  • __stateError: _完成帶有異常的狀態
class _Future<T> implements Future<T> {
  /// Initial state, waiting for a result. In this state, the
  /// [resultOrListeners] field holds a single-linked list of
  /// [_FutureListener] listeners.
  static const int _stateIncomplete = 0;

  /// Pending completion. Set when completed using [_asyncComplete] or
  /// [_asyncCompleteError]. It is an error to try to complete it again.
  /// [resultOrListeners] holds listeners.
  static const int _statePendingComplete = 1;

  /// The future has been chained to another future. The result of that
  /// other future becomes the result of this future as well.
  /// [resultOrListeners] contains the source future.
  static const int _stateChained = 2;

  /// The future has been completed with a value result.
  static const int _stateValue = 4;

  /// The future has been completed with an error result.
  static const int _stateError = 8;

  /** Whether the future is complete, and as what. */
  int _state = _stateIncomplete;
  ...
}
複製程式碼

5. 如何使用Future

5.1 Future的基本使用

  • 1. factory Future(FutureOr computation())

Future的簡單建立就可以通過它的建構函式來建立,通過傳入一個非同步執行Function.

//Future的factory建構函式
factory Future(FutureOr<T> computation()) {
  _Future<T> result = new _Future<T>();
  Timer.run(() {//內部建立了一個Timer
    try {
      result._complete(computation());
    } catch (e, s) {
      _completeWithErrorCallback(result, e, s);
    }
  });
  return result;
}
複製程式碼
void main() {
  print('main is executed start');
  var function = () {
    print('future is executed');
  };
  Future(function);
  print('main is executed end');
}
//或者直接傳入一個匿名函式
void main() {
  print('main is executed start');
  var future = Future(() {
    print('future is executed');
  });
  print('main is executed end');
}
複製程式碼

輸出結果: image.png 從輸出結果可以發現Future輸出是一個非同步的過程,所以 future is executed 輸出在 main is executed end 輸出之後。這是因為main方法中的普通程式碼都是同步執行的,所以是先把main方法中 main is executed start 和 main is executed end 輸出,等到main方法執行結束後就會開始檢查 MicroTask 佇列中是否存在task, 如果有就去執行,直到檢查到 MicroTask Queue 為空,那麼就會去檢查 Event Queue ,由於Future的本質是在內部開啟了一個 Timer 實現非同步,最終這個非同步事件是會放入到 Event Queue 中的,此時正好檢查到當前 Future ,這時候的Event Loop就會去處理執行這個 Future 。所以最後輸出了 future is executed .

  • 2. Future.value()

建立一個返回指定value值的Future物件, 注意在value內部實際上實現非同步是通過 scheduleMicrotask . 之前文章也說過實現Future非同步方式無非只有兩種,一種是使用 Timer 另一種就是使用scheduleMicrotask

void main() {
  var commonFuture = Future((){
    print('future is executed');
  });
  var valueFuture = Future.value(100.0);//
  valueFuture.then((value) => print(value));
  print(valueFuture is Future<double>);
}
複製程式碼

輸出結果: image.png 通過上述輸出結果可以發現,true先輸出來是因為它是同步執行的,也說明最開始同步執行拿到了 valueFuture . 但是為什麼 commonFuture 執行在 valueFuture 執行之後呢,這是因為 Future.value 內部實際上是通過 scheduleMicrotask 實現非同步的,那麼就不難理解了等到main方法執行結束後就會開始檢查 MicroTask 佇列中是否存在task,正好此時的 valueFuture 就是那麼就先執行它,直到檢查到 MicroTask Queue 為空,那麼就會去檢查 Event Queue ,由於Future的本質是在內部開啟了一個 Timer 實現非同步,最終這個非同步事件是會放入到 Event Queue 中的,此時正好檢查到當前 Future ,這時候的Event Loop就會去處理執行這個 Future

不妨來看下 Future.value 原始碼實現:

  factory Future.value([FutureOr<T> value]) {
    return new _Future<T>.immediate(value);//實際上是呼叫了_Future的immediate方法
  }

//進入immediate方法
 _Future.immediate(FutureOr<T> result) : _zone = Zone.current {
    _asyncComplete(result);
  }
//然後再執行_asyncComplete方法
 void _asyncComplete(FutureOr<T> value) {
    assert(!_isComplete);
    if (value is Future<T>) {//如果value是一個Future就把它和當前Future連結起來,很明顯100這個值不是
      _chainFuture(value);
      return;
    }
    _setPendingComplete();//設定PendingComplete狀態
    _zone.scheduleMicrotask(() {//注意了:這裡就是呼叫了scheduleMicrotask方法
      _completeWithValue(value);//最後通過_completeWithValue方法回撥傳入value值
    });
  }
複製程式碼
  • 3. Future.delayed()

建立一個延遲執行的future。實際上內部就是通過建立一個延遲的 Timer 來實現延遲非同步操作。  Future.delayed 主要傳入兩個引數一個是 Duration 延遲時長,另一個就是非同步執行的Function。

void main() {
  var delayedFuture = Future.delayed(Duration(seconds: 3), (){//延遲3s
    print('this is delayed future');
  });
  print('main is executed, waiting a delayed output....');
}
複製程式碼

Future.delayed 方法就是使用了一個延遲的 Timer 實現的,具體可以看看原始碼實現:

factory Future.delayed(Duration duration, [FutureOr<T> computation()]) {
    _Future<T> result = new _Future<T>();
    new Timer(duration, () {//建立一個延遲的Timer,傳遞到Event Queue中,最終被EventLoop處理
      if (computation == null) {
        result._complete(null);
      } else {
        try {
          result._complete(computation());
        } catch (e, s) {
          _completeWithErrorCallback(result, e, s);
        }
      }
    });
    return result;
  }
複製程式碼

5.2 Future的進階使用

  • 1. Future的forEach方法

forEach方法就是根據某個集合,建立一系列的Future,然後再按照建立的順序執行這些Future. Future.forEach有兩個引數:一個是 Iterable 集合物件,另一個就是帶有迭代元素引數的Function方法

void main() {
  var futureList = Future.forEach([1, 2, 3, 4, 5], (int element){
    return Future.delayed(Duration(seconds: element), () => print('this is $element'));//每隔1s輸出this is 1, 隔2s輸出this is 2, 隔3s輸出this is 3, ...
  });
}
複製程式碼

輸出結果: image.png

  • 2. Future的any方法

Future的any方法返回的是第一個執行完成的future的結果,不管是否正常返回還是返回一個error.

void main() {
  var futureList = Future.any([3, 4, 1, 2, 5].map((delay) =>
          new Future.delayed(new Duration(seconds: delay), () => delay)))
      .then(print)
      .catchError(print);
}
複製程式碼

輸出結果: image.png

  • 3. Future的doWhile方法

Future.doWhile方法就是重複性地執行某一個動作,直到返回false或者Future,退出迴圈。特別適合於一些遞迴請求子類目的資料。

void main() {
  var totalDelay = 0;
  var delay = 0;

  Future.doWhile(() {
    if (totalDelay > 10) {//超過10s跳出迴圈
      print('total delay: $totalDelay s');
      return false;
    }
    delay += 1;
    totalDelay = totalDelay + delay;
    return new Future.delayed(new Duration(seconds: delay), () {
      print('wait $delay s');
      return true;
    });
  });
}
複製程式碼

輸出結果:

  • 4. Future的wait方法

用來等待多個future完成,並整合它們的結果,有點類似於RxJava中的zip操作。那這樣的結果就有兩種:

  • 若所有future都有正常結果返回:則future的返回結果是所有指定future的結果的集合
  • 若其中一個future有error返回:則future的返回結果是第一個error的值
void main() {
  var requestApi1 = Future.delayed(Duration(seconds: 1), () => 15650);
  var requestApi2 = Future.delayed(Duration(seconds: 2), () => 2340);
  var requestApi3 = Future.delayed(Duration(seconds: 1), () => 130);

  Future.wait({requestApi1, requestApi2, requestApi3})
      .then((List<int> value) => {
        //最後將拿到的結果累加求和
        print('${value.reduce((value, element) => value + element)}')
      });
}
複製程式碼

輸出結果: image.png

//異常處理
void main() {
  var requestApi1 = Future.delayed(Duration(seconds: 1), () => 15650);
  var requestApi2 = Future.delayed(Duration(seconds: 2), () => throw Exception('api2 is error'));
  var requestApi3 = Future.delayed(Duration(seconds: 1), () => throw Exception('api3 is error'));//輸出結果是api3 is error這是因為api3先執行,因為api2延遲2s

  Future.wait({requestApi1, requestApi2, requestApi3})
      .then((List<int> value) => {
        //最後將拿到的結果累加求和
        print('${value.reduce((value, element) => value + element)}')
      });
}
複製程式碼

輸出結果: image.png

  • 5. Future的microtask方法

我們都知道Future一般都是會把事件加入到Event Queue中,但是Future.microtask方法提供一種方式將事件加入到Microtask Queue中,建立一個在microtask佇列執行的future。 在上面講過,microtask佇列的優先順序是比event佇列高的,而一般future是在event佇列執行的,所以Future.microtask建立的future會優先於其他future進行執行。

void main() {
  var commonFuture = Future(() {
    print('common future is executed');
  });
  var microtaskFuture = Future.microtask(() => print('microtask future is executed'));
}
複製程式碼

輸出結果: image.png

  • 6. Future的sync方法

Future.sync方法返回的是一個同步的Future, 但是需要注意的是,如果這個Future使用 then 註冊Future的結果就是一個非同步,它會把這個Future加入到MicroTask Queue中。

void main () {
  Future.sync(() => print('sync is executed!'));
  print('main is executed!');
}
複製程式碼

輸出結果: image.png 如果使用then來註冊監聽Future的結果,那麼就是非同步的就會把這個Future加入到MicroTask Queue中。

void main() {
  Future.delayed(Duration(seconds: 1), () => print('this is delayed future'));//普通Future會加入Event Queue中
  Future.sync(() => 100).then(print);//sync的Future需要加入microtask Queue中
  print('main is executed!');
}
複製程式碼

image.png

5.3 處理Future返回的結果

  • 1. Future.then方法

Future一般使用 then 方法來註冊Future回撥,需要注意的Future返回的也是一個Future物件,所以可以使用鏈式呼叫使用Future。這樣就可以將前一個Future的輸出結果作為後一個Future的輸入,可以寫成鏈式呼叫。

void main() {
  Future.delayed(Duration(seconds: 1), () => 100)
      .then((value) => Future.delayed(Duration(seconds: 1), () => 100 + value))
      .then((value) => Future.delayed(Duration(seconds: 1), () => 100 + value))
      .then((value) => Future.delayed(Duration(seconds: 1), () => 100 + value))
      .then(print);//最後輸出累加結果就是400
}
複製程式碼

輸出結果: image.png

  • 2. Future.catchError方法

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

void main() {
  Future.delayed(Duration(seconds: 1), () => throw Exception('this is custom error'))
      .catchError(print);//catchError回撥返回異常的Future
}
複製程式碼

輸出結果: image.png

  • 3. Future.whenComplete方法

Future.whenComplete方法有點類似異常捕獲中的try-catch-finally中的finally, 一個Future不管是正常回撥了結果還是丟擲了異常最終都會回撥 whenComplete 方法。

//with value
void main() {
  Future.value(100)
      .then((value) => print(value))
      .whenComplete(() => print('future is completed!'));
  print('main is executed');
}

//with error
void main() {
  Future.delayed(
          Duration(seconds: 1), () => throw Exception('this is custom error'))
      .catchError(print)
      .whenComplete(() => print('future is completed!'));
  print('main is executed');
}
複製程式碼

輸出結果: image.png image.png

6. Future使用的場景

由上述有關Future的介紹,我相信大家對於Future使用場景應該心中有個底,這裡就簡單總結一下:

  • 1、對於Dart中一般實現非同步的場景都可以使用Future,特別是處理多個Future的問題,包括前後依賴關係的Future之間處理,以及聚合多個Future的處理。
  • 2、對於Dart中一般實現非同步的場景單個Future的處理,可以使用async和await
  • 3、對於Dart中比較耗時的任務,不建議使用Future這時候還是使用isolate.

7. 熊喵先生的小總結

到這裡有關非同步程式設計中Future就介紹完畢了,Future相比isolate更加輕量級,很容易輕鬆地實現非同步。而且相比aysnc,await方法在鏈式呼叫方面更具有優勢。但是需要注意的是如果遇到耗時任務比較重的,還是建議使用isolate,因為畢竟Future還是跑在主執行緒中的。還有需要注意一點需要深刻理解EventLoop的概念,比如Event Queue, MicroTask Queue的優先順序。因為Dart後面很多高階非同步API都是建立事件迴圈基礎上的。

感謝關注,熊喵先生願和你在技術路上一起成長!

相關文章