Flutter非同步程式設計-async和await

熊喵先生發表於2021-03-14

async和await實際上是Dart非同步程式設計用於簡化非同步API操作的兩個關鍵字。它的作用就是能夠將非同步的程式碼使用同步的程式碼結構實現。相信學習過之前的Future和Stream的文章就知道對於最終返回的值或者是異常都是採用**非同步回撥方式。**然而async-await就是為了簡化這些非同步回撥的方式,通過語法糖的簡化,將原來非同步回撥方式寫成簡單的同步方式結構。需要注意的是: 使用await關鍵字必須配合async關鍵字一起使用才會起作用。本質上async-await是相當於都Future相關API介面的另一種封裝,提供了一種更加簡便的操作Future相關API的方法。

1. 為什麼需要async-await

通過學習之前非同步程式設計中的Future我們知道,Future一般使用 then 和 catchError 可以很好地處理資料回撥和異常回撥。這實際上還是一種基於非同步回撥的方式,如果非同步操作依賴關係比較複雜需要編寫回撥程式碼比較繁雜,為了簡化這些步驟 async-await 關鍵字通過同步程式碼結構來實現非同步操作,從而使得程式碼更加簡潔和具有可讀性,此外在異常處理方式也會變得更加簡單。

1.1 對比實現程式碼

  • Future實現方式
void main() {
  _loadUserFromSQL().then((userInfo) {
    return _fetchSessionToken(userInfo);
  }).then((token) {
    return _fetchData(token);
  }).then((data){
    print('$data');
  });
  print('main is executed!');
}

class UserInfo {
  final String userName;
  final String pwd;
  bool isValid;

  UserInfo(this.userName, this.pwd);
}

//從本地SQL讀取使用者資訊
Future<UserInfo> _loadUserFromSQL() {
  return Future.delayed(
      Duration(seconds: 2), () => UserInfo('gitchat', '123456'));
}

//獲取使用者token
Future<String> _fetchSessionToken(UserInfo userInfo) {
  return Future.delayed(Duration(seconds: 2), '3424324sfdsfsdf24324234');
}

//請求資料
Future<String> _fetchData(String token) {
  return Future.delayed(
      Duration(seconds: 2),
      () => token.isNotEmpty
          ? Future.value('this is data')
          : Future.error('this is error'));
}
複製程式碼

輸出結果: image.png

  • async-await實現方式
void main() async {//注意:需要新增async,因為await必須在async方法內才有效
  var userInfo = await _loadUserFromSQL();
  var token = await _fetchSessionToken(userInfo);
  var data = await _fetchData(token);
  print('$data');
  print('main is executed!');
}

class UserInfo {
  final String userName;
  final String pwd;
  bool isValid;

  UserInfo(this.userName, this.pwd);
}

//從本地SQL讀取使用者資訊
Future<UserInfo> _loadUserFromSQL() {
  return Future.delayed(
      Duration(seconds: 2), () => UserInfo('gitchat', '123456'));
}

//獲取使用者token
Future<String> _fetchSessionToken(UserInfo userInfo) {
  return Future.delayed(Duration(seconds: 2), () => '3424324sfdsfsdf24324234');
}

//請求資料
Future<String> _fetchData(String token) {
  return Future.delayed(
      Duration(seconds: 2),
      () => token.isNotEmpty
          ? Future.value('this is data')
          : Future.error('this is error'));
}
複製程式碼

輸出結果: image.png

1.2 對比異常下處理

  • Future的實現
void main() {
  _loadUserFromSQL().then((userInfo) {
    return _fetchSessionToken(userInfo);
  }).then((token) {
    return _fetchData(token);
  }).catchError((e) {
    print('fetch data is error: $e');
  }).whenComplete(() => 'all is done');
  print('main is executed!');
}
複製程式碼
  • async-await的實現
void main() async {
  //注意:需要新增async,因為await必須在async方法內才有效
  try {
    var userInfo = await _loadUserFromSQL();
    var token = await _fetchSessionToken(userInfo);
    var data = await _fetchData(token);
    print('$data');
  } on Exception catch (e) {
    print('this is error: $e');
  } finally {
    print('all is done');
  }
  print('main is executed!');
}
複製程式碼

通過對比發現使用Future.then相比async-await呼叫鏈路更清晰,基於非同步回撥的方式呼叫相比比較清晰。而在程式碼實現以及同步結構分析async-await顯得更加簡單且處理異常也更加方面,更加符合同步程式碼的邏輯。

2. 什麼是async-await

2.1 async-await基本介紹

async-await本質上是對Future API的簡化形式,將非同步回撥程式碼寫成同步程式碼結構形式。async關鍵字修飾的函式總是返回一個Future物件,所以async並不會阻塞當前執行緒,由前面的EventLoop和Future我們都知道Future的最終會加入EventQueue中,而EventQueue執行是當main函式執行完畢後,才會檢查Microtask Queue和Event Queue並處理它們。await關鍵字意味著中斷當前程式碼執行流程直到當前的async方法執行完畢,如果沒有執行完畢下面的程式碼將處於等待的狀態。但是需要遵循下面兩個規則:

  • 要定義非同步函式,請在函式主體之前新增async關鍵字
  • **await關鍵字只有在async關鍵字修飾的函式才會有效

**

void main() {
  print("executedFuture return ${executedFuture() is Future}");//所以輸出true
  print('main is executed!');
}

executedFuture() async {//async函式預設返回的是Future物件
  print('executedFuture start');
  await Future.delayed(Duration(seconds: 1), () => print('future is finished'));//await等待Future執行完畢
  print('Future is executed end');//executedFuture end 輸出必須等待await的Future結束後才會執行
}
複製程式碼

輸出結果: image.png 分析下輸出結果,首先main函式同步執行executedFuture函式和print函式,所以馬上就會同步輸出 “executedFuture start”,但是由於executedFuture是一個async函式,await等待一個Future, 在executedFuture函式作用域內,所以並且在await後面執行,所以需要等待Future資料到了才會執行後面語句,但是此時的executedFuture執行完畢,馬上就執行了main函式中的**“main is executed!”, main執行結束後,就會去檢查MicroTask Queue是否存在需要執行的微任務,如果沒有就會繼續檢查Event Queue是否存在需要處理的Event(其中Future也是一種Event), 所以檢查到executedFuture函式中的Future,就會把它交給EventLoop處理,Future執行完畢後,輸出“future is finished”, 最後執行輸出“Future is executed end”。** ** 如果把上述例子修改一下:

void main() async {//main函式變成一個async函式
  await executedFuture(); //executedFuture加入await關鍵字
  print('main is executed!');
}

executedFuture() async {
  print('executedFuture start');
  await Future.delayed(Duration(seconds: 1), () => print('future is finished'));
  print('Future is executed end');//executedFuture end 輸出必須等待await的Future結束後才會執行
}
複製程式碼

輸出結果: image.png 分析輸出結果,可能很多人都很疑惑為什麼**“main is executed”**會輸出在最後,其實很容易理解,因為這時候main函式變成了一個async函式,所以必須等待 await executedFuture() 執行完畢後才會執行後面的 print('main is executed!') . 如果executedFuture沒有執行完畢那麼整個main函式後面程式碼只能等待中,所以一般沒有直接給整個main函式加async關鍵字,這樣會使得整個main函式強行變成了同步執行。

2.2 結合EventLoop理解async-await

通過上面介紹我們知道async-await本質實際上還是Future,本質還是通過向Event Queue中新增Event,然後EventLoop來處理它。但是它們在程式碼結構形式上完全不一樣它是怎麼做到的呢?一起來看下。先給出一個例子:

  • Future.then的理解
class DataWrapper {
  final String data;

  DataWrapper(this.data);
}

Future<DataWrapper> createData() {
  return _loadDataFromDisk().then((id) {
    return _requestNetworkData(id);
  }).then((data) {
    return DataWrapper(data);
  });
}

Future<String> _loadDataFromDisk() {
  return Future.delayed(Duration(seconds: 2), () => '1001');
}

Future<String> _requestNetworkData(String id) {
  return Future.delayed(
      Duration(seconds: 2), () => 'this is id:$id data from network');
}
複製程式碼

其實通過Future的鏈式呼叫執行邏輯可以把 createData 程式碼按事件進行拆分成下面形式, 可以看到createData函式分為1、2、3: image.png 首先第1塊是當 createData 函式被呼叫後,就同步呼叫執行 _loadDataFromDisk 函式,它返回的是一個Future物件,然後就是等待資料的到達EventLoop, 這樣 _loadDataFromDisk 函式就執行結束了。 image.png 然後,當_loadDataFromDisk 函式Future的資料到來時,會觸發第2塊中的 then 函式回撥,拿到id後就立即執行了 _requestNetworkData 函式發出一個HTTP請求,並且先返回一個Future物件,然後就是等待HTTP資料到達EventLoop被處理即可,這樣_requestNetworkData 函式就結束了。 image.png 最後,當 _requestNetworkData  函式Future的資料到來時, 第二個 then 函式就被回撥觸發了,然後就是建立 DataWrapper 返回最終資料物件即可。那麼整個createData函式返回的Future最終就返回真實資料,如果外部呼叫者呼叫了這個函式,那麼在它的 then 函式中就能拿到最終的DataWrapper

  • async-await的理解
class DataWrapper {
  final String data;

  DataWrapper(this.data);
}

Future<DataWrapper> createData() async {//createData新增async關鍵字,表示這是一個非同步函式
  var id = await _loadDataFromDisk(); //await執行_loadDataFromDisk從磁碟中獲取到id
  var data = await _requestNetworkData(id); //通過傳入_loadDataFromDisk的id執行_requestNetworkData返回data
  return DataWrapper(data); //最後返回DataWrapper物件
}

Future<String> _loadDataFromDisk() {
  return Future.delayed(Duration(seconds: 2), () => '1001');
}

Future<String> _requestNetworkData(String id) {
  return Future.delayed(
      Duration(seconds: 2), () => 'this is id:$id data from network');
}
複製程式碼

其實通過async-await的類似同步呼叫執行順序邏輯也可以把 createData 程式碼按事件進行拆分成下面形式,只不過之前是通過 then 函式來斷開,這裡通過await關鍵字來斷開分析, 可以看到createData函式分為1、2、3: image.png 首先,當createData函式開始執行就會觸發第一個等待,此時createData就會將自己Future物件返回給呼叫函式,注意:這裡遇到第1個await等待並呼叫_loadDataFromDisk函式的時候,createData函式就會把自己Future物件返回給呼叫函式, 此時的createData函式就已經執行完畢,可能大家比較疑惑沒有顯式看到返回了一個Future物件,這是因為async關鍵字語法糖幫你做了。有的人又會疑惑下面第3步不是返回了嗎?請注意:下面返回的是 DataWrapper 物件不是一個 Future<DataWrapper> ,所以當執行createData函式的時候碰到第1個await等待,就會馬上return一個Future物件,然後createData函式就執行完畢了。觸發第一個等待並且就執行 _loadDataFromDisk 函式,它返回的是一個Future物件, 然後就會一直等待者資料 id 到來。 image.png 然後,執行第2塊程式碼,當_loadDataFromDisk 函式,它返回的是一個Future物件中的 id 資料到來時,就會觸發第2個await等待並且呼叫_requestNetworkData  函式,發出HTTP網路請求並且先返回一個Future,然後使用await等待這個HTTP Future的到來。 image.png 最後,_requestNetworkData  函式返回Future中的資料到來後,就能拿到HTTP的data資料,最後返回一個DataWrapper, 那麼整個createData函式的Future就拿到最終的資料DataWrapper。  

3. 如何使用async-await

3.1 基本使用

通過對比一般同步實現、非同步Future.then的實現、非同步async-await實現,可以更好地理解async-await用法,使用async-await實際上就是相當於把它當作同步程式碼結構形式來寫即可。下面也是以上面例子為例

  • 同步實現

假設_loadDataFromDisk和_requestNetworkData函式都是同步執行的,那麼就能很容易寫成它們執行程式碼:

DataWrapper createData() {
  var id = _loadDataFromDisk();//同步執行直接return id
  var data = _requestNetworkData(id);//同步執行直接return data
  return DataWrapper(data);
}
複製程式碼
  • 非同步Future.then實現
Future<DataWrapper> createData() {//由於是非同步執行,所以注意返回的物件是一個Future
  return _loadDataFromDisk().then((id) {//需要在非同步回撥then函式中拿到id
    return _requestNetworkData(id);
  }).then((data) {//需要在非同步回撥then函式中拿到data
    return DataWrapper(data);//最後返回最終的DataWrapper
  });
}
複製程式碼
  • 非同步async-await實現

整體程式碼結構形式和同步程式碼結構形式一模一樣只是加了async-await關鍵字以及最後返回的是一個Future物件

Future<DataWrapper> createData() async {//createData新增async關鍵字,表示這是一個非同步函式
  //注意: createData遇到第1個await _loadDataFromDisk,就會先返回一個Future<DataWrapper>物件,結束了createData函式。
  var id = await _loadDataFromDisk(); //await執行_loadDataFromDisk從磁碟中獲取到id
  var data = await _requestNetworkData(id); //通過傳入_loadDataFromDisk的id執行_requestNetworkData返回data
  return DataWrapper(data); //最後返回DataWrapper物件
}
複製程式碼

3.2 異常處理

  • 同步實現

同步執行程式碼實現異常處理也只能是我們常用的try-catch-finally。

DataWrapper createData() {
  try {
    var id = _loadDataFromDisk();
    var data = _requestNetworkData(id);
    return DataWrapper(data);
  } on Exception catch (e) {
    print('this is error');
  } finally {
    print('executed done');
  }
}
複製程式碼
  • 非同步Future.then實現

Future.then實現非同步異常的捕獲一般是藉助 catchError 來實現的。

Future<DataWrapper> createData() {
  return _loadDataFromDisk().then((id) {
    return _requestNetworkData(id);
  }).then((data) {
    return DataWrapper(data);
  }).catchError((e) {//catchError捕獲異常
    print('this is error: $e');
  }).whenComplete((){
    print('executed is done');
  });
}
複製程式碼
  • 非同步async-await實現

async-await實現非同步異常的捕獲和同步實現一模一樣。

Future<DataWrapper> createData() async {
  try {
    var id = await _loadDataFromDisk();
    var data = await _requestNetworkData(id);
    return DataWrapper(data);
  } on Exception catch (e) {
    print('this is error: $e');
  } finally {
    print('executed is done');
  }
}
複製程式碼

4. async-await使用場景

其實關於async-await的使用場景, 學完上面的內容基本上都可以總結分析出來。

  • 大部分Future使用的場景都可以使用async-await來替代,也建議使用async-await,畢竟人家這倆關鍵字就是為了簡化Future API的呼叫,而且能將非同步程式碼寫成同步程式碼形式。程式碼簡潔度上也能得到提升。
  • 其實通過上面例子也能明顯發現,對於一些依賴關係比較明顯的Future,建議還是使用Future,畢竟鏈式呼叫功能非常強大,可以一眼就能看到每個Future之間的前後依賴關係。因為通過async-await雖然簡化了回撥形式,但是在某種程度上降低了Future之間的依賴關係。
  • 對於依賴關係不明顯且彼此獨立Future,可以使用async-await。

5. 熊喵先生的小總結

到這裡有關Dart非同步程式設計中的async-await使用到這裡就結束了,實際上async-await就是一個語法糖的使用,本質還是對Future API的使用,它的好處和優勢就是可以將非同步執行寫成同步程式碼結構形式,我們也對它的程式碼結構進行拆分分析,清晰地分析了其語法糖背後的原理。這樣對使用async-await非常有幫助。

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

相關文章