那些你不知道的Dart細節之帶你透徹理解異步

前言

上週一口氣寫了五篇Dart基礎文章,建議看這篇文章之前先看一下前幾篇文章:

那些你不知道的Dart細節之變量

那些你不知道的Dart細節之內置類型

那些你不知道的Dart細節之函數(方法)

那些你不知道的Dart細節之操作符、流程控制語句、異常

那些你不知道的Dart細節之類的點點滴滴

那些你不知道的Dart細節之泛型和庫

好了,廢話不多說,開始進入今天的主題吧!

async和await

開始說這兩個關鍵字之前我覺得有必要提一下:在Dart中沒有子線程一說,所有代碼都是在一條主線上運行的,所以需要用異步來實現一些耗時操作。(如果非要開啓多線程需要使用隔離,這裏不做敘述)

來說一下這兩個關鍵字吧,async用來修飾方法,需要寫在方法括號的後面,await寫在方法裏面,這裏要注意:await關鍵字必須在async函數內部使用,不然會報錯。await表達式可以使用多次。,這裏其實很好理解:都不是異步方法了你還等待啥啊?下面看一個簡單的樣例吧:

void main() {
  getName1();
  getName2();
  getName3();
}

getName1() async {
  await getStr1();
  await getStr2();
  print('getName1');
}

getStr1() {
  print('getStr1');
}

getStr2() {
  print('getStr2');
}

getName2() {
  print('getName2');
}

getName3() {
  print('getName3');
}

上面這段代碼並不長,大家可以猜一下打印出來的值。

咱們來一步一步分析吧,後面再貼出來答案,看看大家猜的對不對。

首先執行getName1(),執行的時候發現這個方法是async的方法,繼續執行,執行到方法中第一行的時候,發現調用了一個getStr1()方法,而且這個方法使用了await來修飾,表示需要等待執行,重點來了:當遇到await的時候會執行完這一行,打印出了getStr1,之後立即返回一個Future(void)對象(上面的代碼中省略了這個,寫代碼時推薦加上,方便代碼閱讀理解),然後將這個方法中剩餘的代碼放入了事件隊列,接着往下執行getName2()和getName3(),分別打印出了getName2和getName3,剛纔也說過,在Dart中只有一個main線程一桶到底,還有一個事件隊列,現在main線程中都已經執行完畢,但是事件隊列中還有東西,繼續執行getStr2(),執行的時候發現還是await,再進行等待,等待執行完成後打印getStr2,最後再打印getName1

下面是打印出的結果:

getStr1
getName2
getName3
getStr2
getName1

和大家猜的一樣嗎?爲什麼會這樣打印上面已經進行了分析。下面咱們看一下其他的幾個關鍵字。

then,catchError,whenComplete

上面分析中說過,async方法中遇到await時即會返回一個Future對象,從字面上也能知道這個一個未來的值,那麼肯定需要等待完成之後才能獲取到裏面的值。then關鍵字的意思就是獲取等待執行完畢之後返回的值,光說感覺說不明白,還是來看一段代碼吧:

void main() {
  new Future(() => futureTask())//異步任務的函數
      .then((i) => "result:$i")//任務執行完後的子任務
      .then((m) => print(m)); //其中m爲上個任務執行完後的返回的結果
}

futureTask() {
  return 10;
}

上面這段代碼中只有一個打印,下面是打印出的值:

result:10

爲什麼不只顯示10呢?因爲第二次then的時候參數m是第一次then返回的值,而不是futureTask()返回的10這裏應該不難理解。

在then之後還可以拋異常,下面來拋一個異常來看看:

new Future(() => futureTask())//異步任務的函數
      .then((i) => "result:$i")//任務執行完後的子任務
      .then((m) => print(m)) //其中m爲上個任務執行完後的返回的結果
      .then((_) => Future.error("出錯了"));

從上面代碼中可以知道拋異常的方法Future.error(“出錯了”),來看一下執行情況:

result:10
Unhandled exception:
出錯了
#0      _rootHandleUncaughtError.<anonymous closure> (dart:async/zone.dart:1114:29)
#1      _microtaskLoop (dart:async/schedule_microtask.dart:43:21)
#2      _startMicrotaskLoop (dart:async/schedule_microtask.dart:52:5)
#3      _Timer._runTimers (dart:isolate-patch/timer_impl.dart:393:30)
#4      _Timer._handleMessage (dart:isolate-patch/timer_impl.dart:418:5)
#5      _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:174:12)

既然可以拋異常了,當然也可以catch異常,直接使用catchError關鍵字來捕獲一下:

new Future(() => futureTask())//異步任務的函數
      .then((i) => "result:$i")//任務執行完後的子任務
      .then((m) => print(m)) //其中m爲上個任務執行完後的返回的結果
      .then((_) => Future.error("出錯了"))
      .catchError(print);

寫法很簡單,直接參照上面代碼寫即可,下面是運行結果:

result:10
出錯了

👌,已經捕獲了一場,需要做什麼事的可以在裏面進行操作了。

對了,標題中還有一個關鍵字沒說,whenComplete,這個關鍵字的意思簡單,指所有任務完成後的回調函數,使用也很簡單,直接看代碼:

new Future(() => futureTask())//異步任務的函數
      .then((i) => "result:$i")//任務執行完後的子任務
      .then((m) => print(m)) //其中m爲上個任務執行完後的返回的結果
      .then((_) => Future.error("出錯了"))
      .catchError(print)
      .whenComplete(() => print("whenComplete"));//所有任務完成後的回調函數

看一下運行結果:

result:10
出錯了
whenComplete

很簡單對吧?下面說的就有點噁心了。。。🤢

Event-Looper

上面兩張圖說的就是Dart的Event-Looper。其實也不難理解:

  • 一個消息循環的職責就是不斷從消息隊列中取出消息並處理他們直到消息隊列爲空。
  • 消息隊列中的消息可能來自用戶輸入,文件I/O消息,定時器等。例如上圖的消息隊列就包含了定時器消息和用戶輸入消息。
  • Dart中的Main Isolate只有一個Event Looper,但是存在兩個Event Queue: Event Queue以及Microtask Queue。

簡單瞭解一下就行,只要記着它就是一個消息隊列,如果有值就一直循環取出進行處理就夠了。第三點說的事件隊列和微隊列在下面說吧,這裏沒有例子不太好理解。

new Future()

到這裏本文的核心知識點就來了,首先來一張我自己的手繪圖吧:

在這裏插入圖片描述

嗯。。。不要在意畫的,看重點:Dart中方法執行無外乎這三個地方,主線程main,事件隊列(就是上面說的 Event Queue)和微隊列(上面的Microtask Queue),執行順序不太一樣樣,先執行main線程,然後是微隊列,最後是事件隊列。這裏有一個坑,咱們從下面的代碼中來徹底理解一下:

void main(){
  testFuture();
}
void testFuture() {
  Future f = new Future(() => print('f1'));
  Future f1 = new Future(() => null);
  Future f2 = new Future(() => null);
  Future f3 = new Future(() => null);
  f3.then((_) => print('f2'));
  f2.then((_) {
    print('f3');
    new Future(() => print('f4'));
    f1.then((_) {
      print('f5');
    });
  });
  f1.then((m) {
    print('f6');
  });
  print('f7');
}

還是和上面一樣,大家來猜一下運行結果,我先來分析,最後我會把運行結果貼出來,不想看分析的可以直接看運行結果查看一下自己的判斷是否正確。

首先,main中執行了方法testFuture(),在testFuture()方法中在main線程的只有print(‘f7’),其他的都在事件隊列,所以先打印了f7***,main線程已經執行完畢,再來看事件隊列,執行事件隊列之前需要查看一下微隊列中是否有東西,如果微隊列中有東西的話需要先執行*(這就是上面所說的坑),來看一下現在的隊列中都有啥吧:

在這裏插入圖片描述

事件隊列中現在依次放着f、f1、f2和f3,f直接打印了,ok,這裏打印出了f1f1、f2和f3還在事件隊列中,這裏有一些迷惑,代碼中我先放的是f3的then,但是執行的時候並不是先執行f3的then,而是根據事件隊列中的順序來執行的,事件隊列中的f已經執行完畢,接下來該執行f1,f1打印出了f6,f1也執行完畢,該執行f2,f2的then中東西就比較多了,這裏也有今天的重中之重。執行f2的then,先打印出了f3,然後又新增了一個Future,記着,這裏不是直接執行,而是將新增的f4也放入了事件隊列中,再往下執行,用到了f1的then,但是f1咱們剛纔已經執行完畢了,這裏又對f1進行了調用,那麼這個時候就需要把f1放入微隊列中,再來看一下現在的隊列中都有啥:

還記得剛纔說的嗎?本來f2咱們已經執行完成該執行f3了,但是現在微隊列中有了東西,咱們就需要先執行微隊列中的東西,OK,又打印出了f5,接着執行事件隊列中的f3,打印出了f2,最後執行f4,打印出了f4

到這裏就基本分析完了,看完分析的應該已經知道打印的值了,來看一下吧:

f7
f1
f6
f3
f5
f2
f4

和咱們分析的一樣,這裏需要注意的就是微隊列和事件隊列的關係

scheduleMicrotask()

上面代碼中f1進入了微隊列是因爲執行完畢之後再執行,故而放入了微隊列中,這是被動進去的,就不能主動放入微隊列中嗎?當然可以,使用scheduleMicrotask()就可以放入微隊列中。

經過上面的一段代碼相信大家對異步已經有了一定了解,那麼下面再來一段稍微複雜點的加上了scheduleMicrotask()的代碼:

void main(){
  testScheduleMicrotask();
}
void testScheduleMicrotask(){
  scheduleMicrotask(() => print('s1'));//微任務
  //delay 延遲
  new Future.delayed(new Duration(seconds: 1), () => print('s2'));

  new Future(() => print('s3')).then((_) {
    print('s4');
    scheduleMicrotask(() => print('s5'));
  }).then((_) => print('s6'));

  new Future(() => print('s7'));

  scheduleMicrotask(() => print('s8'));

  print('s9');
}

還是和上面一樣,還是同樣的mian線程、事件隊列和微隊列,還是先來分析代碼,最後再貼出運行結果。

上面代碼中main線程中只有一個print(‘s9’),那麼就先打印s9,然後有兩個主動創建的微隊列,s2進行了延遲,這裏注意,進行了延遲直接放入隊列的末尾,所以說最後一個打印的是s2。然後s3放入了事件隊列的第一個,s7放入了第二個,再來看一下現在的隊列吧:

在執行事件隊列前先看一下微隊列中是否有東西,ok,微隊列中有,先來執行微隊列,分別打印出了s1和s8,再來執行事件隊列s3,先打印出了s3,再打印出了s4,又出現了微隊列s5,這裏注意:先將s5放入微隊列中,並不是直接執行,而是執行完s3的事件隊列之後,在執行s7之前再去查看微隊列是否有值,來看一下現在的隊列樣式:

所以這裏又打印了s6,再打印了微隊列中的s5,之後執行事件隊列中的s7,打印出了s7,最後再打印出延時操作的s2

好了,分析完畢,運行結果已經出來了,來看一下吧:

s9
s1
s8
s3
s4
s6
s5
s7
s2

這裏有幾點需要大家注意:

  • 如果可以,儘量將任務放入event隊列中
  • 使用Future的then方法或whenComplete方法來指定任務順序
  • 爲了保持你app的可響應性,儘量不要將大計算量的任務放入這兩個隊列
  • 大計算量的任務放入額外的isolate中(isolate就是上面提到了隔離,這裏不做解釋。)

總結

好了,到這裏本篇文章就到尾聲了。本篇文章帶大家一起看了一下Dart中的異步執行順序,只要理解了上面的兩端代碼,其實Dart的異步你已經基本掌握了,剩下的只有多寫代碼去練、去理解纔行。這是我Dart的第七篇文章了,也是Dart基礎的最後一篇文章,前六篇大家可以通過文章開頭的鏈接去訪問,如果文章對你有幫助,別忘記點贊關注;如果對文章有異議,可以在下面評論區指出來,共同學習進步,不勝感激。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章